diff options
Diffstat (limited to 'test/functional')
109 files changed, 4685 insertions, 1416 deletions
diff --git a/test/functional/README.md b/test/functional/README.md index 82b30fed51..2764acbf18 100644 --- a/test/functional/README.md +++ b/test/functional/README.md @@ -34,7 +34,7 @@ don't have test cases for. - When subclassing the BitcoinTestFramework, place overrides for the `set_test_params()`, `add_options()` and `setup_xxxx()` methods at the top of the subclass, then locally-defined helper methods, then the `run_test()` method. -- Use `'{}'.format(x)` for string formatting, not `'%s' % x`. +- Use `f'{x}'` for string formatting in preference to `'{}'.format(x)` or `'%s' % x`. #### Naming guidelines diff --git a/test/functional/data/wallets/high_minversion/.walletlock b/test/functional/data/wallets/high_minversion/.walletlock deleted file mode 100644 index e69de29bb2..0000000000 --- a/test/functional/data/wallets/high_minversion/.walletlock +++ /dev/null diff --git a/test/functional/data/wallets/high_minversion/GENERATE.md b/test/functional/data/wallets/high_minversion/GENERATE.md deleted file mode 100644 index e55c4557ca..0000000000 --- a/test/functional/data/wallets/high_minversion/GENERATE.md +++ /dev/null @@ -1,8 +0,0 @@ -The wallet has been created by starting Bitcoin Core with the options -`-regtest -datadir=/tmp -nowallet -walletdir=$(pwd)/test/functional/data/wallets/`. - -In the source code, `WalletFeature::FEATURE_LATEST` has been modified to be large, so that the minversion is too high -for a current build of the wallet. - -The wallet has then been created with the RPC `createwallet high_minversion true true`, so that a blank wallet with -private keys disabled is created. diff --git a/test/functional/data/wallets/high_minversion/db.log b/test/functional/data/wallets/high_minversion/db.log deleted file mode 100644 index e69de29bb2..0000000000 --- a/test/functional/data/wallets/high_minversion/db.log +++ /dev/null diff --git a/test/functional/data/wallets/high_minversion/wallet.dat b/test/functional/data/wallets/high_minversion/wallet.dat Binary files differdeleted file mode 100644 index 99ab809263..0000000000 --- a/test/functional/data/wallets/high_minversion/wallet.dat +++ /dev/null diff --git a/test/functional/example_test.py b/test/functional/example_test.py index 3b9bd3048f..c28bb7115f 100755 --- a/test/functional/example_test.py +++ b/test/functional/example_test.py @@ -25,7 +25,6 @@ from test_framework.p2p import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, ) # P2PInterface is a class containing callbacks to be executed when a P2P @@ -115,7 +114,7 @@ class ExampleTest(BitcoinTestFramework): # In this test, we're not connecting node2 to node0 or node1. Calls to # sync_all() should not include node2, since we're not expecting it to # sync. - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_all(self.nodes[0:2]) # Use setup_nodes() to customize the node start behaviour (for example if @@ -183,7 +182,7 @@ class ExampleTest(BitcoinTestFramework): self.nodes[1].waitforblockheight(11) self.log.info("Connect node2 and node1") - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) self.log.info("Wait for node2 to receive all the blocks from node1") self.sync_all() diff --git a/test/functional/feature_abortnode.py b/test/functional/feature_abortnode.py index 17fbf50cc8..8abfdef3a1 100755 --- a/test/functional/feature_abortnode.py +++ b/test/functional/feature_abortnode.py @@ -11,7 +11,7 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import get_datadir_path, connect_nodes +from test_framework.util import get_datadir_path import os @@ -36,7 +36,7 @@ class AbortNodeTest(BitcoinTestFramework): # attempt. self.nodes[1].generate(3) with self.nodes[0].assert_debug_log(["Failed to disconnect block"]): - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.nodes[1].generate(1) # Check that node0 aborted diff --git a/test/functional/feature_backwards_compatibility.py b/test/functional/feature_backwards_compatibility.py index daefb161ac..b161c71a85 100755 --- a/test/functional/feature_backwards_compatibility.py +++ b/test/functional/feature_backwards_compatibility.py @@ -27,6 +27,7 @@ from test_framework.descriptors import descsum_create from test_framework.util import ( assert_equal, + assert_raises_rpc_error, ) @@ -36,13 +37,14 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): self.num_nodes = 6 # Add new version after each release: self.extra_args = [ - ["-addresstype=bech32", "-wallet="], # Pre-release: use to mine blocks + ["-addresstype=bech32"], # Pre-release: use to mine blocks ["-nowallet", "-walletrbf=1", "-addresstype=bech32"], # Pre-release: use to receive coins, swap wallets, etc ["-nowallet", "-walletrbf=1", "-addresstype=bech32"], # v0.19.1 ["-nowallet", "-walletrbf=1", "-addresstype=bech32"], # v0.18.1 ["-nowallet", "-walletrbf=1", "-addresstype=bech32"], # v0.17.2 ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-wallet=wallet.dat"], # v0.16.3 ] + self.wallet_names = [self.default_wallet_name] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -59,6 +61,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): ]) self.start_nodes() + self.import_deterministic_coinbase_privkeys() def run_test(self): self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress()) @@ -80,7 +83,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w1: regular wallet, created on master: update this test when default # wallets can no longer be opened by older versions. - node_master.rpc.createwallet(wallet_name="w1") + node_master.createwallet(wallet_name="w1") wallet = node_master.get_wallet_rpc("w1") info = wallet.getwalletinfo() assert info['private_keys_enabled'] @@ -125,7 +128,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w2: wallet with private keys disabled, created on master: update this # test when default wallets private keys disabled can no longer be # opened by older versions. - node_master.rpc.createwallet(wallet_name="w2", disable_private_keys=True) + node_master.createwallet(wallet_name="w2", disable_private_keys=True) wallet = node_master.get_wallet_rpc("w2") info = wallet.getwalletinfo() assert info['private_keys_enabled'] == False @@ -147,7 +150,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w3: blank wallet, created on master: update this # test when default blank wallets can no longer be opened by older versions. - node_master.rpc.createwallet(wallet_name="w3", blank=True) + node_master.createwallet(wallet_name="w3", blank=True) wallet = node_master.get_wallet_rpc("w3") info = wallet.getwalletinfo() assert info['private_keys_enabled'] @@ -213,67 +216,89 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): os.path.join(node_v19_wallets_dir, wallet) ) - # Open the wallets in v0.19 - node_v19.loadwallet("w1") - wallet = node_v19.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - txs = wallet.listtransactions() - assert_equal(len(txs), 5) - assert_equal(txs[1]["txid"], tx1_id) - assert_equal(txs[2]["walletconflicts"], [tx1_id]) - assert_equal(txs[1]["replaced_by_txid"], tx2_id) - assert not(txs[1]["abandoned"]) - assert_equal(txs[1]["confirmations"], -1) - assert_equal(txs[2]["blockindex"], 1) - assert txs[3]["abandoned"] - assert_equal(txs[4]["walletconflicts"], [tx3_id]) - assert_equal(txs[3]["replaced_by_txid"], tx4_id) - assert not(hasattr(txs[3], "blockindex")) - - node_v19.loadwallet("w2") - wallet = node_v19.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - - node_v19.loadwallet("w3") - wallet = node_v19.get_wallet_rpc("w3") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] == 0 - - # Open the wallets in v0.18 - node_v18.loadwallet("w1") - wallet = node_v18.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - txs = wallet.listtransactions() - assert_equal(len(txs), 5) - assert_equal(txs[1]["txid"], tx1_id) - assert_equal(txs[2]["walletconflicts"], [tx1_id]) - assert_equal(txs[1]["replaced_by_txid"], tx2_id) - assert not(txs[1]["abandoned"]) - assert_equal(txs[1]["confirmations"], -1) - assert_equal(txs[2]["blockindex"], 1) - assert txs[3]["abandoned"] - assert_equal(txs[4]["walletconflicts"], [tx3_id]) - assert_equal(txs[3]["replaced_by_txid"], tx4_id) - assert not(hasattr(txs[3], "blockindex")) - - node_v18.loadwallet("w2") - wallet = node_v18.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - - node_v18.loadwallet("w3") - wallet = node_v18.get_wallet_rpc("w3") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] == 0 + if not self.options.descriptors: + # Descriptor wallets break compatibility, only run this test for legacy wallet + # Open the wallets in v0.19 + node_v19.loadwallet("w1") + wallet = node_v19.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + txs = wallet.listtransactions() + assert_equal(len(txs), 5) + assert_equal(txs[1]["txid"], tx1_id) + assert_equal(txs[2]["walletconflicts"], [tx1_id]) + assert_equal(txs[1]["replaced_by_txid"], tx2_id) + assert not(txs[1]["abandoned"]) + assert_equal(txs[1]["confirmations"], -1) + assert_equal(txs[2]["blockindex"], 1) + assert txs[3]["abandoned"] + assert_equal(txs[4]["walletconflicts"], [tx3_id]) + assert_equal(txs[3]["replaced_by_txid"], tx4_id) + assert not(hasattr(txs[3], "blockindex")) + + node_v19.loadwallet("w2") + wallet = node_v19.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + + node_v19.loadwallet("w3") + wallet = node_v19.get_wallet_rpc("w3") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] == 0 + + # Open the wallets in v0.18 + node_v18.loadwallet("w1") + wallet = node_v18.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + txs = wallet.listtransactions() + assert_equal(len(txs), 5) + assert_equal(txs[1]["txid"], tx1_id) + assert_equal(txs[2]["walletconflicts"], [tx1_id]) + assert_equal(txs[1]["replaced_by_txid"], tx2_id) + assert not(txs[1]["abandoned"]) + assert_equal(txs[1]["confirmations"], -1) + assert_equal(txs[2]["blockindex"], 1) + assert txs[3]["abandoned"] + assert_equal(txs[4]["walletconflicts"], [tx3_id]) + assert_equal(txs[3]["replaced_by_txid"], tx4_id) + assert not(hasattr(txs[3], "blockindex")) + + node_v18.loadwallet("w2") + wallet = node_v18.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + + node_v18.loadwallet("w3") + wallet = node_v18.get_wallet_rpc("w3") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] == 0 + + node_v17.loadwallet("w1") + wallet = node_v17.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + + node_v17.loadwallet("w2") + wallet = node_v17.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + else: + # Descriptor wallets appear to be corrupted wallets to old software + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w1") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w2") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w3") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w1") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w2") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w3") # Open the wallets in v0.17 node_v17.loadwallet("w1_v18") @@ -282,24 +307,12 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): assert info['private_keys_enabled'] assert info['keypoolsize'] > 0 - node_v17.loadwallet("w1") - wallet = node_v17.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - node_v17.loadwallet("w2_v18") wallet = node_v17.get_wallet_rpc("w2_v18") info = wallet.getwalletinfo() assert info['private_keys_enabled'] == False assert info['keypoolsize'] == 0 - node_v17.loadwallet("w2") - wallet = node_v17.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - # RPC loadwallet failure causes bitcoind to exit, in addition to the RPC # call failure, so the following test won't work: # assert_raises_rpc_error(-4, "Wallet loading failed.", node_v17.loadwallet, 'w3_v18') @@ -307,14 +320,22 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # Instead, we stop node and try to launch it with the wallet: self.stop_node(4) node_v17.assert_start_raises_init_error(["-wallet=w3_v18"], "Error: Error loading w3_v18: Wallet requires newer version of Bitcoin Core") - node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: Error loading w3: Wallet requires newer version of Bitcoin Core") + if self.options.descriptors: + # Descriptor wallets appear to be corrupted wallets to old software + node_v17.assert_start_raises_init_error(["-wallet=w1"], "Error: wallet.dat corrupt, salvage failed") + node_v17.assert_start_raises_init_error(["-wallet=w2"], "Error: wallet.dat corrupt, salvage failed") + node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: wallet.dat corrupt, salvage failed") + else: + node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: Error loading w3: Wallet requires newer version of Bitcoin Core") self.start_node(4) - # Open most recent wallet in v0.16 (no loadwallet RPC) - self.restart_node(5, extra_args=["-wallet=w2"]) - wallet = node_v16.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['keypoolsize'] == 1 + if not self.options.descriptors: + # Descriptor wallets break compatibility, only run this test for legacy wallets + # Open most recent wallet in v0.16 (no loadwallet RPC) + self.restart_node(5, extra_args=["-wallet=w2"]) + wallet = node_v16.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['keypoolsize'] == 1 # Create upgrade wallet in v0.16 self.restart_node(-1, extra_args=["-wallet=u1_v16"]) diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py index 1253c45418..60492350ee 100755 --- a/test/functional/feature_bip68_sequence.py +++ b/test/functional/feature_bip68_sequence.py @@ -6,7 +6,7 @@ import time -from test_framework.blocktools import create_block, create_coinbase, add_witness_commitment +from test_framework.blocktools import create_block, NORMAL_GBT_REQUEST_PARAMS, add_witness_commitment from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, ToHex from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -275,6 +275,8 @@ class BIP68Test(BitcoinTestFramework): # Advance the time on the node so that we can test timelocks self.nodes[0].setmocktime(cur_time+600) + # Save block template now to use for the reorg later + tmpl = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) self.nodes[0].generate(1) assert tx2.hash not in self.nodes[0].getrawmempool() @@ -318,16 +320,15 @@ class BIP68Test(BitcoinTestFramework): # diagram above). # This would cause tx2 to be added back to the mempool, which in turn causes # tx3 to be removed. - tip = int(self.nodes[0].getblockhash(self.nodes[0].getblockcount()-1), 16) - height = self.nodes[0].getblockcount() for i in range(2): - block = create_block(tip, create_coinbase(height), cur_time) - block.nVersion = 3 + block = create_block(tmpl=tmpl, ntime=cur_time) block.rehash() block.solve() tip = block.sha256 - height += 1 assert_equal(None if i == 1 else 'inconclusive', self.nodes[0].submitblock(ToHex(block))) + tmpl = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + tmpl['previousblockhash'] = '%x' % tip + tmpl['transactions'] = [] cur_time += 1 mempool = self.nodes[0].getrawmempool() @@ -375,9 +376,7 @@ class BIP68Test(BitcoinTestFramework): assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, ToHex(tx3)) # make a block that violates bip68; ensure that the tip updates - tip = int(self.nodes[0].getbestblockhash(), 16) - block = create_block(tip, create_coinbase(self.nodes[0].getblockcount()+1)) - block.nVersion = 3 + block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) block.vtx.extend([tx1, tx2, tx3]) block.hashMerkleRoot = block.calc_merkle_root() block.rehash() diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 19753d73ef..158efb52c9 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -119,7 +119,7 @@ class FullBlockTest(BitcoinTestFramework): # Allow the block to mature blocks = [] for i in range(NUM_BUFFER_BLOCKS_TO_GENERATE): - blocks.append(self.next_block("maturitybuffer.{}".format(i))) + blocks.append(self.next_block(f"maturitybuffer.{i}")) self.save_spendable_output() self.send_blocks(blocks) @@ -151,8 +151,8 @@ class FullBlockTest(BitcoinTestFramework): if template.valid_in_block: continue - self.log.info("Reject block with invalid tx: %s", TxTemplate.__name__) - blockname = "for_invalid.%s" % TxTemplate.__name__ + self.log.info(f"Reject block with invalid tx: {TxTemplate.__name__}") + blockname = f"for_invalid.{TxTemplate.__name__}" badblock = self.next_block(blockname) badtx = template.get_tx() if TxTemplate != invalid_txs.InputMissing: @@ -1251,7 +1251,7 @@ class FullBlockTest(BitcoinTestFramework): blocks = [] spend = out[32] for i in range(89, LARGE_REORG_SIZE + 89): - b = self.next_block(i, spend, version=4) + b = self.next_block(i, spend) tx = CTransaction() script_length = MAX_BLOCK_BASE_SIZE - len(b.serialize()) - 69 script_output = CScript([b'\x00' * script_length]) @@ -1270,18 +1270,18 @@ class FullBlockTest(BitcoinTestFramework): self.move_tip(88) blocks2 = [] for i in range(89, LARGE_REORG_SIZE + 89): - blocks2.append(self.next_block("alt" + str(i), version=4)) + blocks2.append(self.next_block("alt" + str(i))) self.send_blocks(blocks2, False, force_send=True) # extend alt chain to trigger re-org - block = self.next_block("alt" + str(chain1_tip + 1), version=4) + block = self.next_block("alt" + str(chain1_tip + 1)) self.send_blocks([block], True, timeout=2440) # ... and re-org back to the first chain self.move_tip(chain1_tip) - block = self.next_block(chain1_tip + 1, version=4) + block = self.next_block(chain1_tip + 1) self.send_blocks([block], False, force_send=True) - block = self.next_block(chain1_tip + 2, version=4) + block = self.next_block(chain1_tip + 2) self.send_blocks([block], True, timeout=2440) self.log.info("Reject a block with an invalid block header version") @@ -1289,7 +1289,7 @@ class FullBlockTest(BitcoinTestFramework): self.send_blocks([b_v1], success=False, force_send=True, reject_reason='bad-version(0x00000001)', reconnect=True) self.move_tip(chain1_tip + 2) - b_cb34 = self.next_block('b_cb34', version=4) + b_cb34 = self.next_block('b_cb34') b_cb34.vtx[0].vin[0].scriptSig = b_cb34.vtx[0].vin[0].scriptSig[:-1] b_cb34.vtx[0].rehash() b_cb34.hashMerkleRoot = b_cb34.calc_merkle_root() @@ -1323,7 +1323,7 @@ class FullBlockTest(BitcoinTestFramework): tx.rehash() return tx - def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), *, version=1): + def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), *, version=4): if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 @@ -1355,12 +1355,12 @@ class FullBlockTest(BitcoinTestFramework): # save the current tip so it can be spent by a later block def save_spendable_output(self): - self.log.debug("saving spendable output %s" % self.tip.vtx[0]) + self.log.debug(f"saving spendable output {self.tip.vtx[0]}") self.spendable_outputs.append(self.tip) # get an output that we previously marked as spendable def get_spendable_output(self): - self.log.debug("getting spendable output %s" % self.spendable_outputs[0].vtx[0]) + self.log.debug(f"getting spendable output {self.spendable_outputs[0].vtx[0]}") return self.spendable_outputs.pop(0).vtx[0] # move the tip back to a previous block diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 34e856c1ba..3e28dae4b3 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -14,6 +14,7 @@ class ConfArgsTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.supports_cli = False + self.wallet_names = [] def test_config_file_parser(self): # Assume node is stopped @@ -78,6 +79,12 @@ class ConfArgsTest(BitcoinTestFramework): with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf: conf.write('') # clear + def test_invalid_command_line_options(self): + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: No proxy server specified. Use -proxy=<ip> or -proxy=<ip:port>.', + extra_args=['-proxy'], + ) + def test_log_buffer(self): with self.nodes[0].assert_debug_log(expected_msgs=['Warning: parsed potentially confusing double-negative -connect=0\n']): self.start_node(0, extra_args=['-noconnect=0']) @@ -146,6 +153,7 @@ class ConfArgsTest(BitcoinTestFramework): self.test_networkactive() self.test_config_file_parser() + self.test_invalid_command_line_options() # Remove the -datadir argument so it doesn't override the config file self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")] @@ -171,19 +179,15 @@ class ConfArgsTest(BitcoinTestFramework): # Create the directory and ensure the config file now works os.mkdir(new_data_dir) - self.start_node(0, ['-conf='+conf_file, '-wallet=w1']) + self.start_node(0, ['-conf='+conf_file]) self.stop_node(0) assert os.path.exists(os.path.join(new_data_dir, self.chain, 'blocks')) - if self.is_wallet_compiled(): - assert os.path.exists(os.path.join(new_data_dir, self.chain, 'wallets', 'w1')) # Ensure command line argument overrides datadir in conf os.mkdir(new_data_dir_2) self.nodes[0].datadir = new_data_dir_2 - self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file, '-wallet=w2']) + self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file]) assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'blocks')) - if self.is_wallet_compiled(): - assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'wallets', 'w2')) if __name__ == '__main__': diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 39e8bca751..46ba18b9b5 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -150,7 +150,6 @@ class BIP68_112_113Test(BitcoinTestFramework): self.setup_clean_chain = True self.extra_args = [[ '-whitelist=noban@127.0.0.1', - '-blockversion=4', '-addresstype=legacy', '-par=1', # Use only one script thread to get the exact reject reason for testing ]] diff --git a/test/functional/feature_dbcrash.py b/test/functional/feature_dbcrash.py index 4bc47141a1..7a2e35c095 100755 --- a/test/functional/feature_dbcrash.py +++ b/test/functional/feature_dbcrash.py @@ -56,7 +56,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Set -maxmempool=0 to turn off mempool memory sharing with dbcache # Set -rpcservertimeout=900 to reduce socket disconnects in this # long-running test - self.base_args = ["-limitdescendantsize=0", "-maxmempool=0", "-rpcservertimeout=900", "-dbbatchsize=200000", "-wallet="] + self.base_args = ["-limitdescendantsize=0", "-maxmempool=0", "-rpcservertimeout=900", "-dbbatchsize=200000"] # Set different crash ratios and cache sizes. Note that not all of # -dbcache goes to the in-memory coins cache. @@ -66,7 +66,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Node3 is a normal node with default args, except will mine full blocks # and non-standard txs (e.g. txs with "dust" outputs) - self.node3_args = ["-blockmaxweight=4000000", "-acceptnonstdtxn", "-wallet="] + self.node3_args = ["-blockmaxweight=4000000", "-acceptnonstdtxn"] self.extra_args = [self.node0_args, self.node1_args, self.node2_args, self.node3_args] def skip_test_if_missing_module(self): diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index d89eeec400..8f522aee66 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -13,7 +13,7 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, - connect_nodes, + assert_raises_rpc_error, satoshi_round, ) @@ -145,9 +145,9 @@ class EstimateFeeTest(BitcoinTestFramework): # mine non-standard txs (e.g. txs with "dust" outputs) # Force fSendTrickle to true (via whitelist.noban) self.extra_args = [ - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-wallet="], - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=68000", "-wallet="], - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=32000", "-wallet="], + ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1"], + ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=68000"], + ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=32000"], ] def skip_test_if_missing_module(self): @@ -232,9 +232,9 @@ class EstimateFeeTest(BitcoinTestFramework): # so the estimates would not be affected by the splitting transactions self.start_node(1) self.start_node(2) - connect_nodes(self.nodes[1], 0) - connect_nodes(self.nodes[0], 2) - connect_nodes(self.nodes[2], 1) + self.connect_nodes(1, 0) + self.connect_nodes(0, 2) + self.connect_nodes(2, 1) self.sync_all() @@ -263,6 +263,11 @@ class EstimateFeeTest(BitcoinTestFramework): self.log.info("Final estimates after emptying mempools") check_estimates(self.nodes[1], self.fees_per_kb) + self.log.info("Testing that fee estimation is disabled in blocksonly.") + self.restart_node(0, ["-blocksonly"]) + assert_raises_rpc_error(-32603, "Fee estimation disabled", + self.nodes[0].estimatesmartfee, 2) + if __name__ == '__main__': EstimateFeeTest().main() diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index e4ceb62c94..7de9a589be 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -15,7 +15,7 @@ class FilelockTest(BitcoinTestFramework): def setup_network(self): self.add_nodes(self.num_nodes, extra_args=None) - self.nodes[0].start(['-wallet=']) + self.nodes[0].start() self.nodes[0].wait_for_rpc_connection() def run_test(self): @@ -27,10 +27,11 @@ class FilelockTest(BitcoinTestFramework): self.nodes[1].assert_start_raises_init_error(extra_args=['-datadir={}'.format(self.nodes[0].datadir), '-noserver'], expected_msg=expected_msg) if self.is_wallet_compiled(): + self.nodes[0].createwallet(self.default_wallet_name) wallet_dir = os.path.join(datadir, 'wallets') self.log.info("Check that we can't start a second bitcoind instance using the same wallet") expected_msg = "Error: Error initializing wallet database environment" - self.nodes[1].assert_start_raises_init_error(extra_args=['-walletdir={}'.format(wallet_dir), '-wallet=', '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) + self.nodes[1].assert_start_raises_init_error(extra_args=['-walletdir={}'.format(wallet_dir), '-wallet=' + self.default_wallet_name, '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) if __name__ == '__main__': FilelockTest().main() diff --git a/test/functional/feature_minchainwork.py b/test/functional/feature_minchainwork.py index dbff6f15f2..abf87e8f0c 100755 --- a/test/functional/feature_minchainwork.py +++ b/test/functional/feature_minchainwork.py @@ -18,7 +18,7 @@ only succeeds past a given node once its nMinimumChainWork has been exceeded. import time from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import connect_nodes, assert_equal +from test_framework.util import assert_equal # 2 hashes required per regtest block (with no difficulty adjustment) REGTEST_WORK_PER_BLOCK = 2 @@ -39,7 +39,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): # block relay to inbound peers. self.setup_nodes() for i in range(self.num_nodes-1): - connect_nodes(self.nodes[i+1], i) + self.connect_nodes(i+1, i) def run_test(self): # Start building a chain on node0. node2 shouldn't be able to sync until node1's diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 20020c3237..47bc8dbb49 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -9,8 +9,6 @@ from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE, keyhash_to_p2pkh from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, hex_str_to_bytes, ) @@ -41,12 +39,13 @@ class NotificationsTest(BitcoinTestFramework): # -alertnotify and -blocknotify on node0, walletnotify on node1 self.extra_args = [[ - "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), - "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s'))], - ["-blockversion=211", - "-rescan", - "-wallet={}".format(self.wallet), - "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s')))]] + "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), + "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s')), + ], [ + "-rescan", + "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))), + ]] + self.wallet_names = [self.default_wallet_name, self.wallet] super().setup_network() def run_test(self): @@ -75,7 +74,7 @@ class NotificationsTest(BitcoinTestFramework): self.log.info("test -walletnotify after rescan") # restart node to rescan to force wallet notifications self.start_node(1) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) @@ -126,12 +125,12 @@ class NotificationsTest(BitcoinTestFramework): # Bump tx2 as bump2 and generate a block on node 0 while # disconnected, then reconnect and check for notifications on node 1 # about newly confirmed bump2 and newly conflicted tx2. - disconnect_nodes(self.nodes[0], 1) + self.disconnect_nodes(0, 1) bump2 = self.nodes[0].bumpfee(tx2)["txid"] self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) assert_equal(self.nodes[0].gettransaction(bump2)["confirmations"], 1) assert_equal(tx2 in self.nodes[1].getrawmempool(), True) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks() self.expect_wallet_notify([bump2, tx2]) assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1) diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index ff55cb76d9..b0eac7056b 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -14,7 +14,7 @@ Generate 427 more blocks. """ import time -from test_framework.blocktools import create_coinbase, create_block, create_transaction, add_witness_commitment +from test_framework.blocktools import NORMAL_GBT_REQUEST_PARAMS, create_block, create_transaction, add_witness_commitment from test_framework.messages import CTransaction from test_framework.script import CScript from test_framework.test_framework import BitcoinTestFramework @@ -37,23 +37,32 @@ def trueDummy(tx): class NULLDUMMYTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 1 + # Need two nodes only so GBT doesn't complain that it's not connected + self.num_nodes = 2 self.setup_clean_chain = True # 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 = [[ '-segwitheight=432', '-addresstype=legacy', - ]] + ]] * 2 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): - self.address = self.nodes[0].getnewaddress() - self.ms_address = self.nodes[0].addmultisigaddress(1, [self.address])['address'] - self.wit_address = self.nodes[0].getnewaddress(address_type='p2sh-segwit') - self.wit_ms_address = self.nodes[0].addmultisigaddress(1, [self.address], '', 'p2sh-segwit')['address'] + self.nodes[0].createwallet(wallet_name='wmulti', disable_private_keys=True) + wmulti = self.nodes[0].get_wallet_rpc('wmulti') + w0 = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.address = w0.getnewaddress() + self.pubkey = w0.getaddressinfo(self.address)['pubkey'] + self.ms_address = wmulti.addmultisigaddress(1, [self.pubkey])['address'] + self.wit_address = w0.getnewaddress(address_type='p2sh-segwit') + self.wit_ms_address = wmulti.addmultisigaddress(1, [self.pubkey], '', 'p2sh-segwit')['address'] + if not self.options.descriptors: + # Legacy wallets need to import these so that they are watched by the wallet. This is unnecssary (and does not need to be tested) for descriptor wallets + wmulti.importaddress(self.ms_address) + wmulti.importaddress(self.wit_ms_address) self.coinbase_blocks = self.nodes[0].generate(2) # Block 2 coinbase_txid = [] @@ -61,7 +70,6 @@ class NULLDUMMYTest(BitcoinTestFramework): coinbase_txid.append(self.nodes[0].getblock(i)['tx'][0]) self.nodes[0].generate(427) # Block 429 self.lastblockhash = self.nodes[0].getbestblockhash() - self.tip = int("0x" + self.lastblockhash, 0) self.lastblockheight = 429 self.lastblocktime = int(time.time()) + 429 @@ -102,8 +110,10 @@ class NULLDUMMYTest(BitcoinTestFramework): self.block_submit(self.nodes[0], test6txs, True, True) def block_submit(self, node, txs, witness=False, accept=False): - block = create_block(self.tip, create_coinbase(self.lastblockheight + 1), self.lastblocktime + 1) - block.nVersion = 4 + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + assert_equal(tmpl['previousblockhash'], self.lastblockhash) + assert_equal(tmpl['height'], self.lastblockheight + 1) + block = create_block(tmpl=tmpl, ntime=self.lastblocktime + 1) for tx in txs: tx.rehash() block.vtx.append(tx) @@ -114,7 +124,6 @@ class NULLDUMMYTest(BitcoinTestFramework): assert_equal(None if accept else 'block-validation-failed', node.submitblock(block.serialize().hex())) if (accept): assert_equal(node.getbestblockhash(), block.hash) - self.tip = block.sha256 self.lastblockhash = block.hash self.lastblocktime += 1 self.lastblockheight += 1 diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index be323d355e..05b658ed87 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -18,13 +18,16 @@ Test plan: - proxy on IPv6 - Create various proxies (as threads) -- Create bitcoinds that connect to them -- Manipulate the bitcoinds using addnode (onetry) an observe effects +- Create nodes that connect to them +- Manipulate the peer connections using addnode (onetry) and observe effects +- Test the getpeerinfo `network` field for the peer addnode connect to IPv4 addnode connect to IPv6 addnode connect to onion addnode connect to generic DNS name + +- Test getnetworkinfo for each node """ import socket @@ -41,6 +44,16 @@ from test_framework.netutil import test_ipv6_local RANGE_BEGIN = PORT_MIN + 2 * PORT_RANGE # Start after p2p and rpc ports +# Networks returned by RPC getpeerinfo, defined in src/netbase.cpp::GetNetworkName() +NET_UNROUTABLE = "unroutable" +NET_IPV4 = "ipv4" +NET_IPV6 = "ipv6" +NET_ONION = "onion" + +# Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() +NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION}) + + class ProxyTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 @@ -77,23 +90,29 @@ class ProxyTest(BitcoinTestFramework): self.serv3 = Socks5Server(self.conf3) self.serv3.start() - # Note: proxies are not used to connect to local nodes - # this is because the proxy to use is based on CService.GetNetwork(), which return NET_UNROUTABLE for localhost + # Note: proxies are not used to connect to local nodes. This is because the proxy to + # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. args = [ ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'], ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr),'-proxyrandomize=0'], ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'], [] - ] + ] if self.have_ipv6: args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion'] self.add_nodes(self.num_nodes, extra_args=args) self.start_nodes() + def network_test(self, node, addr, network): + for peer in node.getpeerinfo(): + if peer["addr"] == addr: + assert_equal(peer["network"], network) + def node_test(self, node, proxies, auth, test_onion=True): rv = [] - # Test: outgoing IPv4 connection through node - node.addnode("15.61.23.23:1234", "onetry") + addr = "15.61.23.23:1234" + self.log.debug("Test: outgoing IPv4 connection through node for address {}".format(addr)) + node.addnode(addr, "onetry") cmd = proxies[0].queue.get() assert isinstance(cmd, Socks5Command) # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6 @@ -104,10 +123,12 @@ class ProxyTest(BitcoinTestFramework): assert_equal(cmd.username, None) assert_equal(cmd.password, None) rv.append(cmd) + self.network_test(node, addr, network=NET_IPV4) if self.have_ipv6: - # Test: outgoing IPv6 connection through node - node.addnode("[1233:3432:2434:2343:3234:2345:6546:4534]:5443", "onetry") + addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" + self.log.debug("Test: outgoing IPv6 connection through node for address {}".format(addr)) + node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6 @@ -118,10 +139,12 @@ class ProxyTest(BitcoinTestFramework): assert_equal(cmd.username, None) assert_equal(cmd.password, None) rv.append(cmd) + self.network_test(node, addr, network=NET_IPV6) if test_onion: - # Test: outgoing onion connection through node - node.addnode("bitcoinostk4e4re.onion:8333", "onetry") + addr = "bitcoinostk4e4re.onion:8333" + self.log.debug("Test: outgoing onion connection through node for address {}".format(addr)) + node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) assert_equal(cmd.atyp, AddressType.DOMAINNAME) @@ -131,9 +154,11 @@ class ProxyTest(BitcoinTestFramework): assert_equal(cmd.username, None) assert_equal(cmd.password, None) rv.append(cmd) + self.network_test(node, addr, network=NET_ONION) - # Test: outgoing DNS name connection through node - node.addnode("node.noumenon:8333", "onetry") + addr = "node.noumenon:8333" + self.log.debug("Test: outgoing DNS name connection through node for address {}".format(addr)) + node.addnode(addr, "onetry") cmd = proxies[3].queue.get() assert isinstance(cmd, Socks5Command) assert_equal(cmd.atyp, AddressType.DOMAINNAME) @@ -143,6 +168,7 @@ class ProxyTest(BitcoinTestFramework): assert_equal(cmd.username, None) assert_equal(cmd.password, None) rv.append(cmd) + self.network_test(node, addr, network=NET_UNROUTABLE) return rv @@ -169,15 +195,17 @@ class ProxyTest(BitcoinTestFramework): r[x['name']] = x return r - # test RPC getnetworkinfo + self.log.info("Test RPC getnetworkinfo") n0 = networks_dict(self.nodes[0].getnetworkinfo()) - for net in ['ipv4','ipv6','onion']: + assert_equal(NETWORKS, n0.keys()) + for net in NETWORKS: assert_equal(n0[net]['proxy'], '%s:%i' % (self.conf1.addr)) assert_equal(n0[net]['proxy_randomize_credentials'], True) assert_equal(n0['onion']['reachable'], True) n1 = networks_dict(self.nodes[1].getnetworkinfo()) - for net in ['ipv4','ipv6']: + assert_equal(NETWORKS, n1.keys()) + for net in ['ipv4', 'ipv6']: assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr)) assert_equal(n1[net]['proxy_randomize_credentials'], False) assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) @@ -185,17 +213,20 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n1['onion']['reachable'], True) n2 = networks_dict(self.nodes[2].getnetworkinfo()) - for net in ['ipv4','ipv6','onion']: + assert_equal(NETWORKS, n2.keys()) + for net in NETWORKS: assert_equal(n2[net]['proxy'], '%s:%i' % (self.conf2.addr)) assert_equal(n2[net]['proxy_randomize_credentials'], True) assert_equal(n2['onion']['reachable'], True) if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) - for net in ['ipv4','ipv6']: + assert_equal(NETWORKS, n3.keys()) + for net in NETWORKS: assert_equal(n3[net]['proxy'], '[%s]:%i' % (self.conf3.addr)) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) + if __name__ == '__main__': ProxyTest().main() diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index df54ae777b..f09bffe2d4 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -18,8 +18,6 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - connect_nodes, - disconnect_nodes, ) # Rescans start at the earliest block up to 2 hours before a key timestamp, so @@ -81,16 +79,16 @@ class PruneTest(BitcoinTestFramework): # Create nodes 0 and 1 to mine. # Create node 2 to test pruning. - self.full_node_default_args = ["-maxreceivebuffer=20000", "-checkblocks=5", "-wallet="] + self.full_node_default_args = ["-maxreceivebuffer=20000", "-checkblocks=5"] # Create nodes 3 and 4 to test manual pruning (they will be re-started with manual pruning later) # Create nodes 5 to test wallet in prune mode, but do not connect self.extra_args = [ self.full_node_default_args, self.full_node_default_args, - ["-wallet=", "-maxreceivebuffer=20000", "-prune=550"], - ["-wallet=", "-maxreceivebuffer=20000"], - ["-wallet=", "-maxreceivebuffer=20000"], - ["-wallet=", "-prune=550"], + ["-maxreceivebuffer=20000", "-prune=550"], + ["-maxreceivebuffer=20000"], + ["-maxreceivebuffer=20000"], + ["-prune=550"], ] self.rpc_timeout = 120 @@ -102,18 +100,17 @@ class PruneTest(BitcoinTestFramework): self.prunedir = os.path.join(self.nodes[2].datadir, self.chain, 'blocks', '') - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) - connect_nodes(self.nodes[0], 3) - connect_nodes(self.nodes[0], 4) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) + self.connect_nodes(0, 3) + self.connect_nodes(0, 4) self.sync_blocks(self.nodes[0:5]) def setup_nodes(self): self.add_nodes(self.num_nodes, self.extra_args) self.start_nodes() - for n in self.nodes: - n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=False) + self.import_deterministic_coinbase_privkeys() def create_big_chain(self): # Start by creating some coinbases we can spend later @@ -149,8 +146,8 @@ class PruneTest(BitcoinTestFramework): for _ in range(12): # Disconnect node 0 so it can mine a longer reorg chain without knowing about node 1's soon-to-be-stale chain # Node 2 stays connected, so it hears about the stale blocks and then reorg's when node0 reconnects - disconnect_nodes(self.nodes[0], 1) - disconnect_nodes(self.nodes[0], 2) + self.disconnect_nodes(0, 1) + self.disconnect_nodes(0, 2) # Mine 24 blocks in node 1 mine_large_blocks(self.nodes[1], 24) @@ -158,8 +155,8 @@ class PruneTest(BitcoinTestFramework): mine_large_blocks(self.nodes[0], 25) # Create connections in the order so both nodes can see the reorg at the same time - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) self.sync_blocks(self.nodes[0:3]) self.log.info("Usage can be over target because of high stale rate: %d" % calc_usage(self.prunedir)) @@ -188,15 +185,15 @@ class PruneTest(BitcoinTestFramework): self.log.info("New best height: %d" % self.nodes[1].getblockcount()) # Disconnect node1 and generate the new chain - disconnect_nodes(self.nodes[0], 1) - disconnect_nodes(self.nodes[1], 2) + self.disconnect_nodes(0, 1) + self.disconnect_nodes(1, 2) self.log.info("Generating new longer chain of 300 more blocks") self.nodes[1].generate(300) self.log.info("Reconnect nodes") - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) self.sync_blocks(self.nodes[0:3], timeout=120) self.log.info("Verify height on node 2: %d" % self.nodes[2].getblockcount()) @@ -337,7 +334,7 @@ class PruneTest(BitcoinTestFramework): # check that wallet loads successfully when restarting a pruned node after IBD. # this was reported to fail in #7494. self.log.info("Syncing node 5 to test wallet") - connect_nodes(self.nodes[0], 5) + self.connect_nodes(0, 5) nds = [self.nodes[0], self.nodes[5]] self.sync_blocks(nds, wait=5, timeout=300) self.restart_node(5, extra_args=["-prune=550"]) # restart to trigger rescan diff --git a/test/functional/feature_segwit.py b/test/functional/feature_segwit.py index 0842972779..7bd2fc7847 100755 --- a/test/functional/feature_segwit.py +++ b/test/functional/feature_segwit.py @@ -22,7 +22,6 @@ from test_framework.util import ( assert_equal, assert_is_hex_string, assert_raises_rpc_error, - connect_nodes, hex_str_to_bytes, try_rpc, ) @@ -61,14 +60,12 @@ class SegWitTest(BitcoinTestFramework): ], [ "-acceptnonstdtxn=1", - "-blockversion=4", "-rpcserialversion=1", "-segwitheight=432", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", - "-blockversion=536870915", "-segwitheight=432", "-addresstype=legacy", ], @@ -80,7 +77,7 @@ class SegWitTest(BitcoinTestFramework): def setup_network(self): super().setup_network() - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) self.sync_all() def success_mine(self, node, txid, sign, redeem_script=""): diff --git a/test/functional/feature_settings.py b/test/functional/feature_settings.py index c565854bb0..5a0236401d 100755 --- a/test/functional/feature_settings.py +++ b/test/functional/feature_settings.py @@ -17,6 +17,7 @@ class SettingsTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + self.wallet_names = [] def run_test(self): node, = self.nodes @@ -30,19 +31,25 @@ class SettingsTest(BitcoinTestFramework): # Assert settings are parsed and logged with settings.open("w") as fp: - json.dump({"string": "string", "num": 5, "bool": True, "null": None, "list": [6,7]}, fp) + json.dump({"string": "string", "num": 5, "bool": True, "null": None, "list": [6, 7]}, fp) with node.assert_debug_log(expected_msgs=[ + 'Ignoring unknown rw_settings value bool', + 'Ignoring unknown rw_settings value list', + 'Ignoring unknown rw_settings value null', + 'Ignoring unknown rw_settings value num', + 'Ignoring unknown rw_settings value string', 'Setting file arg: string = "string"', 'Setting file arg: num = 5', 'Setting file arg: bool = true', 'Setting file arg: null = null', - 'Setting file arg: list = [6,7]']): + 'Setting file arg: list = [6,7]', + ]): self.start_node(0) self.stop_node(0) # Assert settings are unchanged after shutdown with settings.open() as fp: - assert_equal(json.load(fp), {"string": "string", "num": 5, "bool": True, "null": None, "list": [6,7]}) + assert_equal(json.load(fp), {"string": "string", "num": 5, "bool": True, "null": None, "list": [6, 7]}) # Test invalid json with settings.open("w") as fp: diff --git a/test/functional/feature_signet.py b/test/functional/feature_signet.py index a0e7f3ee6e..96c581dede 100755 --- a/test/functional/feature_signet.py +++ b/test/functional/feature_signet.py @@ -65,6 +65,10 @@ class SignetBasicTest(BitcoinTestFramework): assert_equal(self.nodes[4].submitblock(signet_blocks[0]), 'bad-signet-blksig') + self.log.info("test that signet logs the network magic on node start") + with self.nodes[0].assert_debug_log(["Signet derived magic (message start)"]): + self.restart_node(0) + if __name__ == '__main__': SignetBasicTest().main() diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py new file mode 100755 index 0000000000..6ee2b72c11 --- /dev/null +++ b/test/functional/feature_taproot.py @@ -0,0 +1,1485 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019-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 Taproot softfork (BIPs 340-342) + +from test_framework.blocktools import ( + create_coinbase, + create_block, + add_witness_commitment, + MAX_BLOCK_SIGOPS_WEIGHT, + NORMAL_GBT_REQUEST_PARAMS, + WITNESS_SCALE_FACTOR, +) +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + ToHex, +) +from test_framework.script import ( + ANNEX_TAG, + CScript, + CScriptNum, + CScriptOp, + LEAF_VERSION_TAPSCRIPT, + LegacySignatureHash, + LOCKTIME_THRESHOLD, + MAX_SCRIPT_ELEMENT_SIZE, + OP_0, + OP_1, + OP_2, + OP_3, + OP_4, + OP_5, + OP_6, + OP_7, + OP_8, + OP_9, + OP_10, + OP_11, + OP_12, + OP_16, + OP_2DROP, + OP_2DUP, + OP_CHECKMULTISIG, + OP_CHECKMULTISIGVERIFY, + OP_CHECKSIG, + OP_CHECKSIGADD, + OP_CHECKSIGVERIFY, + OP_CODESEPARATOR, + OP_DROP, + OP_DUP, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + OP_IF, + OP_NOP, + OP_NOT, + OP_NOTIF, + OP_PUSHDATA1, + OP_RETURN, + OP_SWAP, + OP_VERIFY, + SIGHASH_DEFAULT, + SIGHASH_ALL, + SIGHASH_NONE, + SIGHASH_SINGLE, + SIGHASH_ANYONECANPAY, + SegwitV0SignatureHash, + TaprootSignatureHash, + is_op_success, + taproot_construct, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_raises_rpc_error, assert_equal +from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey +from test_framework.address import ( + hash160, + sha256, +) +from collections import OrderedDict, namedtuple +from io import BytesIO +import json +import hashlib +import os +import random + +# === Framework for building spending transactions. === +# +# The computation is represented as a "context" dict, whose entries store potentially-unevaluated expressions that +# refer to lower-level ones. By overwriting these expression, many aspects - both high and low level - of the signing +# process can be overridden. +# +# Specifically, a context object is a dict that maps names to compositions of: +# - values +# - lists of values +# - callables which, when fed the context object as argument, produce any of these +# +# The DEFAULT_CONTEXT object specifies a standard signing process, with many overridable knobs. +# +# The get(ctx, name) function can evaluate a name, and cache its result in the context. +# getter(name) can be used to construct a callable that evaluates name. For example: +# +# ctx1 = {**DEFAULT_CONTEXT, inputs=[getter("sign"), b'\x01']} +# +# creates a context where the script inputs are a signature plus the bytes 0x01. +# +# override(expr, name1=expr1, name2=expr2, ...) can be used to cause an expression to be evaluated in a selectively +# modified context. For example: +# +# ctx2 = {**DEFAULT_CONTEXT, sighash=override(default_sighash, hashtype=SIGHASH_DEFAULT)} +# +# creates a context ctx2 where the sighash is modified to use hashtype=SIGHASH_DEFAULT. This differs from +# +# ctx3 = {**DEFAULT_CONTEXT, hashtype=SIGHASH_DEFAULT} +# +# in that ctx3 will globally use hashtype=SIGHASH_DEFAULT (including in the hashtype byte appended to the signature) +# while ctx2 only uses the modified hashtype inside the sighash calculation. + +def deep_eval(ctx, expr): + """Recursively replace any callables c in expr (including inside lists) with c(ctx).""" + while callable(expr): + expr = expr(ctx) + if isinstance(expr, list): + expr = [deep_eval(ctx, x) for x in expr] + return expr + +# Data type to represent fully-evaluated expressions in a context dict (so we can avoid reevaluating them). +Final = namedtuple("Final", "value") + +def get(ctx, name): + """Evaluate name in context ctx.""" + assert name in ctx, "Missing '%s' in context" % name + expr = ctx[name] + if not isinstance(expr, Final): + # Evaluate and cache the result. + expr = Final(deep_eval(ctx, expr)) + ctx[name] = expr + return expr.value + +def getter(name): + """Return a callable that evaluates name in its passed context.""" + return lambda ctx: get(ctx, name) + +def override(expr, **kwargs): + """Return a callable that evaluates expr in a modified context.""" + return lambda ctx: deep_eval({**ctx, **kwargs}, expr) + +# === Implementations for the various default expressions in DEFAULT_CONTEXT === + +def default_hashtype(ctx): + """Default expression for "hashtype": SIGHASH_DEFAULT for taproot, SIGHASH_ALL otherwise.""" + mode = get(ctx, "mode") + if mode == "taproot": + return SIGHASH_DEFAULT + else: + return SIGHASH_ALL + +def default_tapleaf(ctx): + """Default expression for "tapleaf": looking up leaf in tap[2].""" + return get(ctx, "tap").leaves[get(ctx, "leaf")] + +def default_script_taproot(ctx): + """Default expression for "script_taproot": tapleaf.script.""" + return get(ctx, "tapleaf").script + +def default_leafversion(ctx): + """Default expression for "leafversion": tapleaf.version""" + return get(ctx, "tapleaf").version + +def default_negflag(ctx): + """Default expression for "negflag": tap.negflag.""" + return get(ctx, "tap").negflag + +def default_pubkey_inner(ctx): + """Default expression for "pubkey_inner": tap.inner_pubkey.""" + return get(ctx, "tap").inner_pubkey + +def default_merklebranch(ctx): + """Default expression for "merklebranch": tapleaf.merklebranch.""" + return get(ctx, "tapleaf").merklebranch + +def default_controlblock(ctx): + """Default expression for "controlblock": combine leafversion, negflag, pubkey_inner, merklebranch.""" + return bytes([get(ctx, "leafversion") + get(ctx, "negflag")]) + get(ctx, "pubkey_inner") + get(ctx, "merklebranch") + +def default_sighash(ctx): + """Default expression for "sighash": depending on mode, compute BIP341, BIP143, or legacy sighash.""" + tx = get(ctx, "tx") + idx = get(ctx, "idx") + hashtype = get(ctx, "hashtype_actual") + mode = get(ctx, "mode") + if mode == "taproot": + # BIP341 signature hash + utxos = get(ctx, "utxos") + annex = get(ctx, "annex") + if get(ctx, "leaf") is not None: + codeseppos = get(ctx, "codeseppos") + leaf_ver = get(ctx, "leafversion") + script = get(ctx, "script_taproot") + return TaprootSignatureHash(tx, utxos, hashtype, idx, scriptpath=True, script=script, leaf_ver=leaf_ver, codeseparator_pos=codeseppos, annex=annex) + else: + return TaprootSignatureHash(tx, utxos, hashtype, idx, scriptpath=False, annex=annex) + elif mode == "witv0": + # BIP143 signature hash + scriptcode = get(ctx, "scriptcode") + utxos = get(ctx, "utxos") + return SegwitV0SignatureHash(scriptcode, tx, idx, hashtype, utxos[idx].nValue) + else: + # Pre-segwit signature hash + scriptcode = get(ctx, "scriptcode") + return LegacySignatureHash(scriptcode, tx, idx, hashtype)[0] + +def default_tweak(ctx): + """Default expression for "tweak": None if a leaf is specified, tap[0] otherwise.""" + if get(ctx, "leaf") is None: + return get(ctx, "tap").tweak + return None + +def default_key_tweaked(ctx): + """Default expression for "key_tweaked": key if tweak is None, tweaked with it otherwise.""" + key = get(ctx, "key") + tweak = get(ctx, "tweak") + if tweak is None: + return key + else: + return tweak_add_privkey(key, tweak) + +def default_signature(ctx): + """Default expression for "signature": BIP340 signature or ECDSA signature depending on mode.""" + sighash = get(ctx, "sighash") + if get(ctx, "mode") == "taproot": + key = get(ctx, "key_tweaked") + flip_r = get(ctx, "flag_flip_r") + flip_p = get(ctx, "flag_flip_p") + return sign_schnorr(key, sighash, flip_r=flip_r, flip_p=flip_p) + else: + key = get(ctx, "key") + return key.sign_ecdsa(sighash) + +def default_hashtype_actual(ctx): + """Default expression for "hashtype_actual": hashtype, unless mismatching SIGHASH_SINGLE in taproot.""" + hashtype = get(ctx, "hashtype") + mode = get(ctx, "mode") + if mode != "taproot": + return hashtype + idx = get(ctx, "idx") + tx = get(ctx, "tx") + if hashtype & 3 == SIGHASH_SINGLE and idx >= len(tx.vout): + return (hashtype & ~3) | SIGHASH_NONE + return hashtype + +def default_bytes_hashtype(ctx): + """Default expression for "bytes_hashtype": bytes([hashtype_actual]) if not 0, b"" otherwise.""" + return bytes([x for x in [get(ctx, "hashtype_actual")] if x != 0]) + +def default_sign(ctx): + """Default expression for "sign": concatenation of signature and bytes_hashtype.""" + return get(ctx, "signature") + get(ctx, "bytes_hashtype") + +def default_inputs_keypath(ctx): + """Default expression for "inputs_keypath": a signature.""" + return [get(ctx, "sign")] + +def default_witness_taproot(ctx): + """Default expression for "witness_taproot", consisting of inputs, script, control block, and annex as needed.""" + annex = get(ctx, "annex") + suffix_annex = [] + if annex is not None: + suffix_annex = [annex] + if get(ctx, "leaf") is None: + return get(ctx, "inputs_keypath") + suffix_annex + else: + return get(ctx, "inputs") + [bytes(get(ctx, "script_taproot")), get(ctx, "controlblock")] + suffix_annex + +def default_witness_witv0(ctx): + """Default expression for "witness_witv0", consisting of inputs and witness script, as needed.""" + script = get(ctx, "script_witv0") + inputs = get(ctx, "inputs") + if script is None: + return inputs + else: + return inputs + [script] + +def default_witness(ctx): + """Default expression for "witness", delegating to "witness_taproot" or "witness_witv0" as needed.""" + mode = get(ctx, "mode") + if mode == "taproot": + return get(ctx, "witness_taproot") + elif mode == "witv0": + return get(ctx, "witness_witv0") + else: + return [] + +def default_scriptsig(ctx): + """Default expression for "scriptsig", consisting of inputs and redeemscript, as needed.""" + scriptsig = [] + mode = get(ctx, "mode") + if mode == "legacy": + scriptsig = get(ctx, "inputs") + redeemscript = get(ctx, "script_p2sh") + if redeemscript is not None: + scriptsig += [bytes(redeemscript)] + return scriptsig + +# The default context object. +DEFAULT_CONTEXT = { + # == The main expressions to evaluate. Only override these for unusual or invalid spends. == + # The overall witness stack, as a list of bytes objects. + "witness": default_witness, + # The overall scriptsig, as a list of CScript objects (to be concatenated) and bytes objects (to be pushed) + "scriptsig": default_scriptsig, + + # == Expressions you'll generally only override for intentionally invalid spends. == + # The witness stack for spending a taproot output. + "witness_taproot": default_witness_taproot, + # The witness stack for spending a P2WPKH/P2WSH output. + "witness_witv0": default_witness_witv0, + # The script inputs for a taproot key path spend. + "inputs_keypath": default_inputs_keypath, + # The actual hashtype to use (usually equal to hashtype, but in taproot SIGHASH_SINGLE is not always allowed). + "hashtype_actual": default_hashtype_actual, + # The bytes object for a full signature (including hashtype byte, if needed). + "bytes_hashtype": default_bytes_hashtype, + # A full script signature (bytes including hashtype, if needed) + "sign": default_sign, + # An ECDSA or Schnorr signature (excluding hashtype byte). + "signature": default_signature, + # The 32-byte tweaked key (equal to key for script path spends, or key+tweak for key path spends). + "key_tweaked": default_key_tweaked, + # The tweak to use (None for script path spends, the actual tweak for key path spends). + "tweak": default_tweak, + # The sighash value (32 bytes) + "sighash": default_sighash, + # The information about the chosen script path spend (TaprootLeafInfo object). + "tapleaf": default_tapleaf, + # The script to push, and include in the sighash, for a taproot script path spend. + "script_taproot": default_script_taproot, + # The inner pubkey for a taproot script path spend (32 bytes). + "pubkey_inner": default_pubkey_inner, + # The negation flag of the inner pubkey for a taproot script path spend. + "negflag": default_negflag, + # The leaf version to include in the sighash (this does not affect the one in the control block). + "leafversion": default_leafversion, + # The Merkle path to include in the control block for a script path spend. + "merklebranch": default_merklebranch, + # The control block to push for a taproot script path spend. + "controlblock": default_controlblock, + # Whether to produce signatures with invalid P sign (Schnorr signatures only). + "flag_flip_p": False, + # Whether to produce signatures with invalid R sign (Schnorr signatures only). + "flag_flip_r": False, + + # == Parameters that can be changed without invalidating, but do have a default: == + # The hashtype (as an integer). + "hashtype": default_hashtype, + # The annex (only when mode=="taproot"). + "annex": None, + # The codeseparator position (only when mode=="taproot"). + "codeseppos": -1, + # The redeemscript to add to the scriptSig (if P2SH; None implies not P2SH). + "script_p2sh": None, + # The script to add to the witness in (if P2WSH; None implies P2WPKH) + "script_witv0": None, + # The leaf to use in taproot spends (if script path spend; None implies key path spend). + "leaf": None, + # The input arguments to provide to the executed script + "inputs": [], + + # == Parameters to be set before evaluation: == + # - mode: what spending style to use ("taproot", "witv0", or "legacy"). + # - key: the (untweaked) private key to sign with (ECKey object for ECDSA, 32 bytes for Schnorr). + # - tap: the TaprootInfo object (see taproot_construct; needed in mode=="taproot"). + # - tx: the transaction to sign. + # - utxos: the UTXOs being spent (needed in mode=="witv0" and mode=="taproot"). + # - idx: the input position being signed. + # - scriptcode: the scriptcode to include in legacy and witv0 sighashes. +} + +def flatten(lst): + ret = [] + for elem in lst: + if isinstance(elem, list): + ret += flatten(elem) + else: + ret.append(elem) + return ret + +def spend(tx, idx, utxos, **kwargs): + """Sign transaction input idx of tx, provided utxos is the list of outputs being spent. + + Additional arguments may be provided that override any aspect of the signing process. + See DEFAULT_CONTEXT above for what can be overridden, and what must be provided. + """ + + ctx = {**DEFAULT_CONTEXT, "tx":tx, "idx":idx, "utxos":utxos, **kwargs} + + def to_script(elem): + """If fed a CScript, return it; if fed bytes, return a CScript that pushes it.""" + if isinstance(elem, CScript): + return elem + else: + return CScript([elem]) + + scriptsig_list = flatten(get(ctx, "scriptsig")) + scriptsig = CScript(b"".join(bytes(to_script(elem)) for elem in scriptsig_list)) + witness_stack = flatten(get(ctx, "witness")) + return (scriptsig, witness_stack) + + +# === Spender objects === +# +# Each spender is a tuple of: +# - A scriptPubKey which is to be spent from (CScript) +# - A comment describing the test (string) +# - Whether the spending (on itself) is expected to be standard (bool) +# - A tx-signing lambda returning (scriptsig, witness_stack), taking as inputs: +# - A transaction to sign (CTransaction) +# - An input position (int) +# - The spent UTXOs by this transaction (list of CTxOut) +# - Whether to produce a valid spend (bool) +# - A string with an expected error message for failure case if known +# - The (pre-taproot) sigops weight consumed by a successful spend +# - Whether this spend cannot fail +# - Whether this test demands being placed in a txin with no corresponding txout (for testing SIGHASH_SINGLE behavior) + +Spender = namedtuple("Spender", "script,comment,is_standard,sat_function,err_msg,sigops_weight,no_fail,need_vin_vout_mismatch") + +def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh=False, spk_mutate_pre_p2sh=None, failure=None, standard=True, err_msg=None, sigops_weight=0, need_vin_vout_mismatch=False, **kwargs): + """Helper for constructing Spender objects using the context signing framework. + + * tap: a TaprootInfo object (see taproot_construct), for Taproot spends (cannot be combined with pkh, witv0, or script) + * witv0: boolean indicating the use of witness v0 spending (needs one of script or pkh) + * script: the actual script executed (for bare/P2WSH/P2SH spending) + * pkh: the public key for P2PKH or P2WPKH spending + * p2sh: whether the output is P2SH wrapper (this is supported even for Taproot, where it makes the output unencumbered) + * spk_mutate_pre_psh: a callable to be applied to the script (before potentially P2SH-wrapping it) + * failure: a dict of entries to override in the context when intentionally failing to spend (if None, no_fail will be set) + * standard: whether the (valid version of) spending is expected to be standard + * err_msg: a string with an expected error message for failure (or None, if not cared about) + * sigops_weight: the pre-taproot sigops weight consumed by a successful spend + * need_vin_vout_mismatch: whether this test requires being tested in a transaction input that has no corresponding + transaction output. + """ + + conf = dict() + + # Compute scriptPubKey and set useful defaults based on the inputs. + if witv0: + assert tap is None + conf["mode"] = "witv0" + if pkh is not None: + # P2WPKH + assert script is None + pubkeyhash = hash160(pkh) + spk = CScript([OP_0, pubkeyhash]) + conf["scriptcode"] = CScript([OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]) + conf["script_witv0"] = None + conf["inputs"] = [getter("sign"), pkh] + elif script is not None: + # P2WSH + spk = CScript([OP_0, sha256(script)]) + conf["scriptcode"] = script + conf["script_witv0"] = script + else: + assert False + elif tap is None: + conf["mode"] = "legacy" + if pkh is not None: + # P2PKH + assert script is None + pubkeyhash = hash160(pkh) + spk = CScript([OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]) + conf["scriptcode"] = spk + conf["inputs"] = [getter("sign"), pkh] + elif script is not None: + # bare + spk = script + conf["scriptcode"] = script + else: + assert False + else: + assert script is None + conf["mode"] = "taproot" + conf["tap"] = tap + spk = tap.scriptPubKey + + if spk_mutate_pre_p2sh is not None: + spk = spk_mutate_pre_p2sh(spk) + + if p2sh: + # P2SH wrapper can be combined with anything else + conf["script_p2sh"] = spk + spk = CScript([OP_HASH160, hash160(spk), OP_EQUAL]) + + conf = {**conf, **kwargs} + + def sat_fn(tx, idx, utxos, valid): + if valid: + return spend(tx, idx, utxos, **conf) + else: + assert failure is not None + return spend(tx, idx, utxos, **{**conf, **failure}) + + return Spender(script=spk, comment=comment, is_standard=standard, sat_function=sat_fn, err_msg=err_msg, sigops_weight=sigops_weight, no_fail=failure is None, need_vin_vout_mismatch=need_vin_vout_mismatch) + +def add_spender(spenders, *args, **kwargs): + """Make a spender using make_spender, and add it to spenders.""" + spenders.append(make_spender(*args, **kwargs)) + +# === Helpers for the test === + +def random_checksig_style(pubkey): + """Creates a random CHECKSIG* tapscript that would succeed with only the valid signature on witness stack.""" + return bytes(CScript([pubkey, OP_CHECKSIG])) + opcode = random.choice([OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD]) + if (opcode == OP_CHECKSIGVERIFY): + ret = CScript([pubkey, opcode, OP_1]) + elif (opcode == OP_CHECKSIGADD): + num = random.choice([0, 0x7fffffff, -0x7fffffff]) + ret = CScript([num, pubkey, opcode, num + 1, OP_EQUAL]) + else: + ret = CScript([pubkey, opcode]) + return bytes(ret) + +def random_bytes(n): + """Return a random bytes object of length n.""" + return bytes(random.getrandbits(8) for i in range(n)) + +def bitflipper(expr): + """Return a callable that evaluates expr and returns it with a random bitflip.""" + def fn(ctx): + sub = deep_eval(ctx, expr) + assert isinstance(sub, bytes) + return (int.from_bytes(sub, 'little') ^ (1 << random.randrange(len(sub) * 8))).to_bytes(len(sub), 'little') + return fn + +def zero_appender(expr): + """Return a callable that evaluates expr and returns it with a zero added.""" + return lambda ctx: deep_eval(ctx, expr) + b"\x00" + +def byte_popper(expr): + """Return a callable that evaluates expr and returns it with its last byte removed.""" + return lambda ctx: deep_eval(ctx, expr)[:-1] + +# Expected error strings + +ERR_SIG_SIZE = {"err_msg": "Invalid Schnorr signature size"} +ERR_SIG_HASHTYPE = {"err_msg": "Invalid Schnorr signature hash type"} +ERR_SIG_SCHNORR = {"err_msg": "Invalid Schnorr signature"} +ERR_OP_RETURN = {"err_msg": "OP_RETURN was encountered"} +ERR_CONTROLBLOCK_SIZE = {"err_msg": "Invalid Taproot control block size"} +ERR_WITNESS_PROGRAM_MISMATCH = {"err_msg": "Witness program hash mismatch"} +ERR_PUSH_LIMIT = {"err_msg": "Push value size limit exceeded"} +ERR_DISABLED_OPCODE = {"err_msg": "Attempted to use a disabled opcode"} +ERR_TAPSCRIPT_CHECKMULTISIG = {"err_msg": "OP_CHECKMULTISIG(VERIFY) is not available in tapscript"} +ERR_MINIMALIF = {"err_msg": "OP_IF/NOTIF argument must be minimal in tapscript"} +ERR_UNKNOWN_PUBKEY = {"err_msg": "Public key is neither compressed or uncompressed"} +ERR_STACK_SIZE = {"err_msg": "Stack size limit exceeded"} +ERR_CLEANSTACK = {"err_msg": "Stack size must be exactly one after execution"} +ERR_STACK_EMPTY = {"err_msg": "Operation not valid with the current stack size"} +ERR_SIGOPS_RATIO = {"err_msg": "Too much signature validation relative to witness weight"} +ERR_UNDECODABLE = {"err_msg": "Opcode missing or not understood"} +ERR_NO_SUCCESS = {"err_msg": "Script evaluated without error but finished with a false/empty top stack element"} +ERR_EMPTY_WITNESS = {"err_msg": "Witness program was passed an empty witness"} +ERR_CHECKSIGVERIFY = {"err_msg": "Script failed an OP_CHECKSIGVERIFY operation"} + +VALID_SIGHASHES_ECDSA = [ + SIGHASH_ALL, + SIGHASH_NONE, + SIGHASH_SINGLE, + SIGHASH_ANYONECANPAY + SIGHASH_ALL, + SIGHASH_ANYONECANPAY + SIGHASH_NONE, + SIGHASH_ANYONECANPAY + SIGHASH_SINGLE +] + +VALID_SIGHASHES_TAPROOT = [SIGHASH_DEFAULT] + VALID_SIGHASHES_ECDSA + +VALID_SIGHASHES_TAPROOT_SINGLE = [ + SIGHASH_SINGLE, + SIGHASH_ANYONECANPAY + SIGHASH_SINGLE +] + +VALID_SIGHASHES_TAPROOT_NO_SINGLE = [h for h in VALID_SIGHASHES_TAPROOT if h not in VALID_SIGHASHES_TAPROOT_SINGLE] + +SIGHASH_BITFLIP = {"failure": {"sighash": bitflipper(default_sighash)}} +SIG_POP_BYTE = {"failure": {"sign": byte_popper(default_sign)}} +SINGLE_SIG = {"inputs": [getter("sign")]} +SIG_ADD_ZERO = {"failure": {"sign": zero_appender(default_sign)}} + +DUST_LIMIT = 600 +MIN_FEE = 50000 + +# === Actual test cases === + + +def spenders_taproot_active(): + """Return a list of Spenders for testing post-Taproot activation behavior.""" + + secs = [generate_privkey() for _ in range(8)] + pubs = [compute_xonly_pubkey(sec)[0] for sec in secs] + + spenders = [] + + # == Tests for BIP340 signature validation. == + # These are primarily tested through the test vectors implemented in libsecp256k1, and in src/tests/key_tests.cpp. + # Some things are tested programmatically as well here. + + tap = taproot_construct(pubs[0]) + # Test with key with bit flipped. + add_spender(spenders, "sig/key", tap=tap, key=secs[0], failure={"key_tweaked": bitflipper(default_key_tweaked)}, **ERR_SIG_SCHNORR) + # Test with sighash with bit flipped. + add_spender(spenders, "sig/sighash", tap=tap, key=secs[0], failure={"sighash": bitflipper(default_sighash)}, **ERR_SIG_SCHNORR) + # Test with invalid R sign. + add_spender(spenders, "sig/flip_r", tap=tap, key=secs[0], failure={"flag_flip_r": True}, **ERR_SIG_SCHNORR) + # Test with invalid P sign. + add_spender(spenders, "sig/flip_p", tap=tap, key=secs[0], failure={"flag_flip_p": True}, **ERR_SIG_SCHNORR) + # Test with signature with bit flipped. + add_spender(spenders, "sig/bitflip", tap=tap, key=secs[0], failure={"signature": bitflipper(default_signature)}, **ERR_SIG_SCHNORR) + + # == Tests for signature hashing == + + # Run all tests once with no annex, and once with a valid random annex. + for annex in [None, lambda _: bytes([ANNEX_TAG]) + random_bytes(random.randrange(0, 250))]: + # Non-empty annex is non-standard + no_annex = annex is None + + # Sighash mutation tests (test all sighash combinations) + for hashtype in VALID_SIGHASHES_TAPROOT: + common = {"annex": annex, "hashtype": hashtype, "standard": no_annex} + + # Pure pubkey + tap = taproot_construct(pubs[0]) + add_spender(spenders, "sighash/purepk", tap=tap, key=secs[0], **common, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + + # Pubkey/P2PK script combination + scripts = [("s0", CScript(random_checksig_style(pubs[1])))] + tap = taproot_construct(pubs[0], scripts) + add_spender(spenders, "sighash/keypath_hashtype_%x" % hashtype, tap=tap, key=secs[0], **common, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/scriptpath_hashtype_%x" % hashtype, tap=tap, leaf="s0", key=secs[1], **common, **SINGLE_SIG, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + + # Test SIGHASH_SINGLE behavior in combination with mismatching outputs + if hashtype in VALID_SIGHASHES_TAPROOT_SINGLE: + add_spender(spenders, "sighash/keypath_hashtype_mis_%x" % hashtype, tap=tap, key=secs[0], annex=annex, standard=no_annex, hashtype_actual=random.choice(VALID_SIGHASHES_TAPROOT_NO_SINGLE), failure={"hashtype_actual": hashtype}, **ERR_SIG_HASHTYPE, need_vin_vout_mismatch=True) + add_spender(spenders, "sighash/scriptpath_hashtype_mis_%x" % hashtype, tap=tap, leaf="s0", key=secs[1], annex=annex, standard=no_annex, hashtype_actual=random.choice(VALID_SIGHASHES_TAPROOT_NO_SINGLE), **SINGLE_SIG, failure={"hashtype_actual": hashtype}, **ERR_SIG_HASHTYPE, need_vin_vout_mismatch=True) + + # Test OP_CODESEPARATOR impact on sighashing. + hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT) + common = {"annex": annex, "hashtype": hashtype, "standard": no_annex} + scripts = [ + ("pk_codesep", CScript(random_checksig_style(pubs[1]) + bytes([OP_CODESEPARATOR]))), # codesep after checksig + ("codesep_pk", CScript(bytes([OP_CODESEPARATOR]) + random_checksig_style(pubs[1]))), # codesep before checksig + ("branched_codesep", CScript([random_bytes(random.randrange(511)), OP_DROP, OP_IF, OP_CODESEPARATOR, pubs[0], OP_ELSE, OP_CODESEPARATOR, pubs[1], OP_ENDIF, OP_CHECKSIG])), # branch dependent codesep + ] + random.shuffle(scripts) + tap = taproot_construct(pubs[0], scripts) + add_spender(spenders, "sighash/pk_codesep", tap=tap, leaf="pk_codesep", key=secs[1], **common, **SINGLE_SIG, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/codesep_pk", tap=tap, leaf="codesep_pk", key=secs[1], codeseppos=0, **common, **SINGLE_SIG, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/branched_codesep/left", tap=tap, leaf="branched_codesep", key=secs[0], codeseppos=3, **common, inputs=[getter("sign"), b'\x01'], **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/branched_codesep/right", tap=tap, leaf="branched_codesep", key=secs[1], codeseppos=6, **common, inputs=[getter("sign"), b''], **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + + # Reusing the scripts above, test that various features affect the sighash. + add_spender(spenders, "sighash/annex", tap=tap, leaf="pk_codesep", key=secs[1], hashtype=hashtype, standard=False, **SINGLE_SIG, annex=bytes([ANNEX_TAG]), failure={"sighash": override(default_sighash, annex=None)}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/script", tap=tap, leaf="pk_codesep", key=secs[1], **common, **SINGLE_SIG, failure={"sighash": override(default_sighash, script_taproot=tap.leaves["codesep_pk"].script)}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/leafver", tap=tap, leaf="pk_codesep", key=secs[1], **common, **SINGLE_SIG, failure={"sighash": override(default_sighash, leafversion=random.choice([x & 0xFE for x in range(0x100) if x & 0xFE != 0xC0]))}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/scriptpath", tap=tap, leaf="pk_codesep", key=secs[1], **common, **SINGLE_SIG, failure={"sighash": override(default_sighash, leaf=None)}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/keypath", tap=tap, key=secs[0], **common, failure={"sighash": override(default_sighash, leaf="pk_codesep")}, **ERR_SIG_SCHNORR) + + # Test that invalid hashtypes don't work, both in key path and script path spends + hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT) + for invalid_hashtype in [x for x in range(0x100) if x not in VALID_SIGHASHES_TAPROOT]: + add_spender(spenders, "sighash/keypath_unk_hashtype_%x" % invalid_hashtype, tap=tap, key=secs[0], hashtype=hashtype, failure={"hashtype": invalid_hashtype}, **ERR_SIG_HASHTYPE) + add_spender(spenders, "sighash/scriptpath_unk_hashtype_%x" % invalid_hashtype, tap=tap, leaf="pk_codesep", key=secs[1], **SINGLE_SIG, hashtype=hashtype, failure={"hashtype": invalid_hashtype}, **ERR_SIG_HASHTYPE) + + # Test that hashtype 0 cannot have a hashtype byte, and 1 must have one. + add_spender(spenders, "sighash/hashtype0_byte_keypath", tap=tap, key=secs[0], hashtype=SIGHASH_DEFAULT, failure={"bytes_hashtype": bytes([SIGHASH_DEFAULT])}, **ERR_SIG_HASHTYPE) + add_spender(spenders, "sighash/hashtype0_byte_scriptpath", tap=tap, leaf="pk_codesep", key=secs[1], **SINGLE_SIG, hashtype=SIGHASH_DEFAULT, failure={"bytes_hashtype": bytes([SIGHASH_DEFAULT])}, **ERR_SIG_HASHTYPE) + add_spender(spenders, "sighash/hashtype1_byte_keypath", tap=tap, key=secs[0], hashtype=SIGHASH_ALL, failure={"bytes_hashtype": b''}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/hashtype1_byte_scriptpath", tap=tap, leaf="pk_codesep", key=secs[1], **SINGLE_SIG, hashtype=SIGHASH_ALL, failure={"bytes_hashtype": b''}, **ERR_SIG_SCHNORR) + # Test that hashtype 0 and hashtype 1 cannot be transmuted into each other. + add_spender(spenders, "sighash/hashtype0to1_keypath", tap=tap, key=secs[0], hashtype=SIGHASH_DEFAULT, failure={"bytes_hashtype": bytes([SIGHASH_ALL])}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/hashtype0to1_scriptpath", tap=tap, leaf="pk_codesep", key=secs[1], **SINGLE_SIG, hashtype=SIGHASH_DEFAULT, failure={"bytes_hashtype": bytes([SIGHASH_ALL])}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/hashtype1to0_keypath", tap=tap, key=secs[0], hashtype=SIGHASH_ALL, failure={"bytes_hashtype": b''}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/hashtype1to0_scriptpath", tap=tap, leaf="pk_codesep", key=secs[1], **SINGLE_SIG, hashtype=SIGHASH_ALL, failure={"bytes_hashtype": b''}, **ERR_SIG_SCHNORR) + + # Test aspects of signatures with unusual lengths + for hashtype in [SIGHASH_DEFAULT, random.choice(VALID_SIGHASHES_TAPROOT)]: + scripts = [ + ("csv", CScript([pubs[2], OP_CHECKSIGVERIFY, OP_1])), + ("cs_pos", CScript([pubs[2], OP_CHECKSIG])), + ("csa_pos", CScript([OP_0, pubs[2], OP_CHECKSIGADD, OP_1, OP_EQUAL])), + ("cs_neg", CScript([pubs[2], OP_CHECKSIG, OP_NOT])), + ("csa_neg", CScript([OP_2, pubs[2], OP_CHECKSIGADD, OP_2, OP_EQUAL])) + ] + random.shuffle(scripts) + tap = taproot_construct(pubs[3], scripts) + # Empty signatures + add_spender(spenders, "siglen/empty_keypath", tap=tap, key=secs[3], hashtype=hashtype, failure={"sign": b""}, **ERR_SIG_SIZE) + add_spender(spenders, "siglen/empty_csv", tap=tap, key=secs[2], leaf="csv", hashtype=hashtype, **SINGLE_SIG, failure={"sign": b""}, **ERR_CHECKSIGVERIFY) + add_spender(spenders, "siglen/empty_cs", tap=tap, key=secs[2], leaf="cs_pos", hashtype=hashtype, **SINGLE_SIG, failure={"sign": b""}, **ERR_NO_SUCCESS) + add_spender(spenders, "siglen/empty_csa", tap=tap, key=secs[2], leaf="csa_pos", hashtype=hashtype, **SINGLE_SIG, failure={"sign": b""}, **ERR_NO_SUCCESS) + add_spender(spenders, "siglen/empty_cs_neg", tap=tap, key=secs[2], leaf="cs_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", failure={"sign": lambda _: random_bytes(random.randrange(1, 63))}, **ERR_SIG_SIZE) + add_spender(spenders, "siglen/empty_csa_neg", tap=tap, key=secs[2], leaf="csa_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", failure={"sign": lambda _: random_bytes(random.randrange(66, 100))}, **ERR_SIG_SIZE) + # Appending a zero byte to signatures invalidates them + add_spender(spenders, "siglen/padzero_keypath", tap=tap, key=secs[3], hashtype=hashtype, **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + add_spender(spenders, "siglen/padzero_csv", tap=tap, key=secs[2], leaf="csv", hashtype=hashtype, **SINGLE_SIG, **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + add_spender(spenders, "siglen/padzero_cs", tap=tap, key=secs[2], leaf="cs_pos", hashtype=hashtype, **SINGLE_SIG, **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + add_spender(spenders, "siglen/padzero_csa", tap=tap, key=secs[2], leaf="csa_pos", hashtype=hashtype, **SINGLE_SIG, **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + add_spender(spenders, "siglen/padzero_cs_neg", tap=tap, key=secs[2], leaf="cs_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + add_spender(spenders, "siglen/padzero_csa_neg", tap=tap, key=secs[2], leaf="csa_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", **SIG_ADD_ZERO, **(ERR_SIG_HASHTYPE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SIZE)) + # Removing the last byte from signatures invalidates them + add_spender(spenders, "siglen/popbyte_keypath", tap=tap, key=secs[3], hashtype=hashtype, **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + add_spender(spenders, "siglen/popbyte_csv", tap=tap, key=secs[2], leaf="csv", hashtype=hashtype, **SINGLE_SIG, **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + add_spender(spenders, "siglen/popbyte_cs", tap=tap, key=secs[2], leaf="cs_pos", hashtype=hashtype, **SINGLE_SIG, **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + add_spender(spenders, "siglen/popbyte_csa", tap=tap, key=secs[2], leaf="csa_pos", hashtype=hashtype, **SINGLE_SIG, **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + add_spender(spenders, "siglen/popbyte_cs_neg", tap=tap, key=secs[2], leaf="cs_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + add_spender(spenders, "siglen/popbyte_csa_neg", tap=tap, key=secs[2], leaf="csa_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", **SIG_POP_BYTE, **(ERR_SIG_SIZE if hashtype == SIGHASH_DEFAULT else ERR_SIG_SCHNORR)) + # Verify that an invalid signature is not allowed, not even when the CHECKSIG* is expected to fail. + add_spender(spenders, "siglen/invalid_cs_neg", tap=tap, key=secs[2], leaf="cs_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", failure={"sign": default_sign, "sighash": bitflipper(default_sighash)}, **ERR_SIG_SCHNORR) + add_spender(spenders, "siglen/invalid_csa_neg", tap=tap, key=secs[2], leaf="csa_neg", hashtype=hashtype, **SINGLE_SIG, sign=b"", failure={"sign": default_sign, "sighash": bitflipper(default_sighash)}, **ERR_SIG_SCHNORR) + + # == Test that BIP341 spending only applies to witness version 1, program length 32, no P2SH == + + for p2sh in [False, True]: + for witver in range(1, 17): + for witlen in [20, 31, 32, 33]: + def mutate(spk): + prog = spk[2:] + assert len(prog) == 32 + if witlen < 32: + prog = prog[0:witlen] + elif witlen > 32: + prog += bytes([0 for _ in range(witlen - 32)]) + return CScript([CScriptOp.encode_op_n(witver), prog]) + scripts = [("s0", CScript([pubs[0], OP_CHECKSIG])), ("dummy", CScript([OP_RETURN]))] + tap = taproot_construct(pubs[1], scripts) + if not p2sh and witver == 1 and witlen == 32: + add_spender(spenders, "applic/keypath", p2sh=p2sh, spk_mutate_pre_p2sh=mutate, tap=tap, key=secs[1], **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "applic/scriptpath", p2sh=p2sh, leaf="s0", spk_mutate_pre_p2sh=mutate, tap=tap, key=secs[0], **SINGLE_SIG, failure={"leaf": "dummy"}, **ERR_OP_RETURN) + else: + add_spender(spenders, "applic/keypath", p2sh=p2sh, spk_mutate_pre_p2sh=mutate, tap=tap, key=secs[1], standard=False) + add_spender(spenders, "applic/scriptpath", p2sh=p2sh, leaf="s0", spk_mutate_pre_p2sh=mutate, tap=tap, key=secs[0], **SINGLE_SIG, standard=False) + + # == Test various aspects of BIP341 spending paths == + + # A set of functions that compute the hashing partner in a Merkle tree, designed to exercise + # edge cases. This relies on the taproot_construct feature that a lambda can be passed in + # instead of a subtree, to compute the partner to be hashed with. + PARTNER_MERKLE_FN = [ + # Combine with itself + lambda h: h, + # Combine with hash 0 + lambda h: bytes([0 for _ in range(32)]), + # Combine with hash 2^256-1 + lambda h: bytes([0xff for _ in range(32)]), + # Combine with itself-1 (BE) + lambda h: (int.from_bytes(h, 'big') - 1).to_bytes(32, 'big'), + # Combine with itself+1 (BE) + lambda h: (int.from_bytes(h, 'big') + 1).to_bytes(32, 'big'), + # Combine with itself-1 (LE) + lambda h: (int.from_bytes(h, 'little') - 1).to_bytes(32, 'big'), + # Combine with itself+1 (LE) + lambda h: (int.from_bytes(h, 'little') + 1).to_bytes(32, 'little'), + # Combine with random bitflipped version of self. + lambda h: (int.from_bytes(h, 'little') ^ (1 << random.randrange(256))).to_bytes(32, 'little') + ] + # Start with a tree of that has depth 1 for "128deep" and depth 2 for "129deep". + scripts = [("128deep", CScript([pubs[0], OP_CHECKSIG])), [("129deep", CScript([pubs[0], OP_CHECKSIG])), random.choice(PARTNER_MERKLE_FN)]] + # Add 127 nodes on top of that tree, so that "128deep" and "129deep" end up at their designated depths. + for _ in range(127): + scripts = [scripts, random.choice(PARTNER_MERKLE_FN)] + tap = taproot_construct(pubs[0], scripts) + # Test that spends with a depth of 128 work, but 129 doesn't (even with a tree with weird Merkle branches in it). + add_spender(spenders, "spendpath/merklelimit", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"leaf": "129deep"}, **ERR_CONTROLBLOCK_SIZE) + # Test that flipping the negation bit invalidates spends. + add_spender(spenders, "spendpath/negflag", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"negflag": lambda ctx: 1 - default_negflag(ctx)}, **ERR_WITNESS_PROGRAM_MISMATCH) + # Test that bitflips in the Merkle branch invalidate it. + add_spender(spenders, "spendpath/bitflipmerkle", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"merklebranch": bitflipper(default_merklebranch)}, **ERR_WITNESS_PROGRAM_MISMATCH) + # Test that bitflips in the inner pubkey invalidate it. + add_spender(spenders, "spendpath/bitflippubkey", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"pubkey_inner": bitflipper(default_pubkey_inner)}, **ERR_WITNESS_PROGRAM_MISMATCH) + # Test that empty witnesses are invalid. + add_spender(spenders, "spendpath/emptywit", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"witness": []}, **ERR_EMPTY_WITNESS) + # Test that adding garbage to the control block invalidates it. + add_spender(spenders, "spendpath/padlongcontrol", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"controlblock": lambda ctx: default_controlblock(ctx) + random_bytes(random.randrange(1, 32))}, **ERR_CONTROLBLOCK_SIZE) + # Test that truncating the control block invalidates it. + add_spender(spenders, "spendpath/trunclongcontrol", tap=tap, leaf="128deep", **SINGLE_SIG, key=secs[0], failure={"controlblock": lambda ctx: default_merklebranch(ctx)[0:random.randrange(1, 32)]}, **ERR_CONTROLBLOCK_SIZE) + + scripts = [("s", CScript([pubs[0], OP_CHECKSIG]))] + tap = taproot_construct(pubs[1], scripts) + # Test that adding garbage to the control block invalidates it. + add_spender(spenders, "spendpath/padshortcontrol", tap=tap, leaf="s", **SINGLE_SIG, key=secs[0], failure={"controlblock": lambda ctx: default_controlblock(ctx) + random_bytes(random.randrange(1, 32))}, **ERR_CONTROLBLOCK_SIZE) + # Test that truncating the control block invalidates it. + add_spender(spenders, "spendpath/truncshortcontrol", tap=tap, leaf="s", **SINGLE_SIG, key=secs[0], failure={"controlblock": lambda ctx: default_merklebranch(ctx)[0:random.randrange(1, 32)]}, **ERR_CONTROLBLOCK_SIZE) + # Test that truncating the control block to 1 byte ("-1 Merkle length") invalidates it + add_spender(spenders, "spendpath/trunc1shortcontrol", tap=tap, leaf="s", **SINGLE_SIG, key=secs[0], failure={"controlblock": lambda ctx: default_merklebranch(ctx)[0:1]}, **ERR_CONTROLBLOCK_SIZE) + + # == Test BIP342 edge cases == + + csa_low_val = random.randrange(0, 17) # Within range for OP_n + csa_low_result = csa_low_val + 1 + + csa_high_val = random.randrange(17, 100) if random.getrandbits(1) else random.randrange(-100, -1) # Outside OP_n range + csa_high_result = csa_high_val + 1 + + OVERSIZE_NUMBER = 2**31 + assert_equal(len(CScriptNum.encode(CScriptNum(OVERSIZE_NUMBER))), 6) + assert_equal(len(CScriptNum.encode(CScriptNum(OVERSIZE_NUMBER-1))), 5) + + big_choices = [] + big_scriptops = [] + for i in range(1000): + r = random.randrange(len(pubs)) + big_choices.append(r) + big_scriptops += [pubs[r], OP_CHECKSIGVERIFY] + + + def big_spend_inputs(ctx): + """Helper function to construct the script input for t33/t34 below.""" + # Instead of signing 999 times, precompute signatures for every (key, hashtype) combination + sigs = {} + for ht in VALID_SIGHASHES_TAPROOT: + for k in range(len(pubs)): + sigs[(k, ht)] = override(default_sign, hashtype=ht, key=secs[k])(ctx) + num = get(ctx, "num") + return [sigs[(big_choices[i], random.choice(VALID_SIGHASHES_TAPROOT))] for i in range(num - 1, -1, -1)] + + # Various BIP342 features + scripts = [ + # 0) drop stack element and OP_CHECKSIG + ("t0", CScript([OP_DROP, pubs[1], OP_CHECKSIG])), + # 1) normal OP_CHECKSIG + ("t1", CScript([pubs[1], OP_CHECKSIG])), + # 2) normal OP_CHECKSIGVERIFY + ("t2", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_1])), + # 3) Hypothetical OP_CHECKMULTISIG script that takes a single sig as input + ("t3", CScript([OP_0, OP_SWAP, OP_1, pubs[1], OP_1, OP_CHECKMULTISIG])), + # 4) Hypothetical OP_CHECKMULTISIGVERIFY script that takes a single sig as input + ("t4", CScript([OP_0, OP_SWAP, OP_1, pubs[1], OP_1, OP_CHECKMULTISIGVERIFY, OP_1])), + # 5) OP_IF script that needs a true input + ("t5", CScript([OP_IF, pubs[1], OP_CHECKSIG, OP_ELSE, OP_RETURN, OP_ENDIF])), + # 6) OP_NOTIF script that needs a true input + ("t6", CScript([OP_NOTIF, OP_RETURN, OP_ELSE, pubs[1], OP_CHECKSIG, OP_ENDIF])), + # 7) OP_CHECKSIG with an empty key + ("t7", CScript([OP_0, OP_CHECKSIG])), + # 8) OP_CHECKSIGVERIFY with an empty key + ("t8", CScript([OP_0, OP_CHECKSIGVERIFY, OP_1])), + # 9) normal OP_CHECKSIGADD that also ensures return value is correct + ("t9", CScript([csa_low_val, pubs[1], OP_CHECKSIGADD, csa_low_result, OP_EQUAL])), + # 10) OP_CHECKSIGADD with empty key + ("t10", CScript([csa_low_val, OP_0, OP_CHECKSIGADD, csa_low_result, OP_EQUAL])), + # 11) OP_CHECKSIGADD with missing counter stack element + ("t11", CScript([pubs[1], OP_CHECKSIGADD, OP_1, OP_EQUAL])), + # 12) OP_CHECKSIG that needs invalid signature + ("t12", CScript([pubs[1], OP_CHECKSIGVERIFY, pubs[0], OP_CHECKSIG, OP_NOT])), + # 13) OP_CHECKSIG with empty key that needs invalid signature + ("t13", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_0, OP_CHECKSIG, OP_NOT])), + # 14) OP_CHECKSIGADD that needs invalid signature + ("t14", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_0, pubs[0], OP_CHECKSIGADD, OP_NOT])), + # 15) OP_CHECKSIGADD with empty key that needs invalid signature + ("t15", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_0, OP_0, OP_CHECKSIGADD, OP_NOT])), + # 16) OP_CHECKSIG with unknown pubkey type + ("t16", CScript([OP_1, OP_CHECKSIG])), + # 17) OP_CHECKSIGADD with unknown pubkey type + ("t17", CScript([OP_0, OP_1, OP_CHECKSIGADD])), + # 18) OP_CHECKSIGVERIFY with unknown pubkey type + ("t18", CScript([OP_1, OP_CHECKSIGVERIFY, OP_1])), + # 19) script longer than 10000 bytes and over 201 non-push opcodes + ("t19", CScript([OP_0, OP_0, OP_2DROP] * 10001 + [pubs[1], OP_CHECKSIG])), + # 20) OP_CHECKSIGVERIFY with empty key + ("t20", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_0, OP_0, OP_CHECKSIGVERIFY, OP_1])), + # 21) Script that grows the stack to 1000 elements + ("t21", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_1] + [OP_DUP] * 999 + [OP_DROP] * 999)), + # 22) Script that grows the stack to 1001 elements + ("t22", CScript([pubs[1], OP_CHECKSIGVERIFY, OP_1] + [OP_DUP] * 1000 + [OP_DROP] * 1000)), + # 23) Script that expects an input stack of 1000 elements + ("t23", CScript([OP_DROP] * 999 + [pubs[1], OP_CHECKSIG])), + # 24) Script that expects an input stack of 1001 elements + ("t24", CScript([OP_DROP] * 1000 + [pubs[1], OP_CHECKSIG])), + # 25) Script that pushes a MAX_SCRIPT_ELEMENT_SIZE-bytes element + ("t25", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE), OP_DROP, pubs[1], OP_CHECKSIG])), + # 26) Script that pushes a (MAX_SCRIPT_ELEMENT_SIZE+1)-bytes element + ("t26", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1), OP_DROP, pubs[1], OP_CHECKSIG])), + # 27) CHECKSIGADD that must fail because numeric argument number is >4 bytes + ("t27", CScript([CScriptNum(OVERSIZE_NUMBER), pubs[1], OP_CHECKSIGADD])), + # 28) Pushes random CScriptNum value, checks OP_CHECKSIGADD result + ("t28", CScript([csa_high_val, pubs[1], OP_CHECKSIGADD, csa_high_result, OP_EQUAL])), + # 29) CHECKSIGADD that succeeds with proper sig because numeric argument number is <=4 bytes + ("t29", CScript([CScriptNum(OVERSIZE_NUMBER-1), pubs[1], OP_CHECKSIGADD])), + # 30) Variant of t1 with "normal" 33-byte pubkey + ("t30", CScript([b'\x03' + pubs[1], OP_CHECKSIG])), + # 31) Variant of t2 with "normal" 33-byte pubkey + ("t31", CScript([b'\x02' + pubs[1], OP_CHECKSIGVERIFY, OP_1])), + # 32) Variant of t28 with "normal" 33-byte pubkey + ("t32", CScript([csa_high_val, b'\x03' + pubs[1], OP_CHECKSIGADD, csa_high_result, OP_EQUAL])), + # 33) 999-of-999 multisig + ("t33", CScript(big_scriptops[:1998] + [OP_1])), + # 34) 1000-of-1000 multisig + ("t34", CScript(big_scriptops[:2000] + [OP_1])), + # 35) Variant of t9 that uses a non-minimally encoded input arg + ("t35", CScript([bytes([csa_low_val]), pubs[1], OP_CHECKSIGADD, csa_low_result, OP_EQUAL])), + # 36) Empty script + ("t36", CScript([])), + ] + # Add many dummies to test huge trees + for j in range(100000): + scripts.append((None, CScript([OP_RETURN, random.randrange(100000)]))) + random.shuffle(scripts) + tap = taproot_construct(pubs[0], scripts) + common = { + "hashtype": hashtype, + "key": secs[1], + "tap": tap, + } + # Test that MAX_SCRIPT_ELEMENT_SIZE byte stack element inputs are valid, but not one more (and 80 bytes is standard but 81 is not). + add_spender(spenders, "tapscript/inputmaxlimit", leaf="t0", **common, standard=False, inputs=[getter("sign"), random_bytes(MAX_SCRIPT_ELEMENT_SIZE)], failure={"inputs": [getter("sign"), random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1)]}, **ERR_PUSH_LIMIT) + add_spender(spenders, "tapscript/input80limit", leaf="t0", **common, inputs=[getter("sign"), random_bytes(80)]) + add_spender(spenders, "tapscript/input81limit", leaf="t0", **common, standard=False, inputs=[getter("sign"), random_bytes(81)]) + # Test that OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY cause failure, but OP_CHECKSIG and OP_CHECKSIGVERIFY work. + add_spender(spenders, "tapscript/disabled_checkmultisig", leaf="t1", **common, **SINGLE_SIG, failure={"leaf": "t3"}, **ERR_TAPSCRIPT_CHECKMULTISIG) + add_spender(spenders, "tapscript/disabled_checkmultisigverify", leaf="t2", **common, **SINGLE_SIG, failure={"leaf": "t4"}, **ERR_TAPSCRIPT_CHECKMULTISIG) + # Test that OP_IF and OP_NOTIF do not accept non-0x01 as truth value (the MINIMALIF rule is consensus in Tapscript) + add_spender(spenders, "tapscript/minimalif", leaf="t5", **common, inputs=[getter("sign"), b'\x01'], failure={"inputs": [getter("sign"), b'\x02']}, **ERR_MINIMALIF) + add_spender(spenders, "tapscript/minimalnotif", leaf="t6", **common, inputs=[getter("sign"), b'\x01'], failure={"inputs": [getter("sign"), b'\x03']}, **ERR_MINIMALIF) + add_spender(spenders, "tapscript/minimalif", leaf="t5", **common, inputs=[getter("sign"), b'\x01'], failure={"inputs": [getter("sign"), b'\x0001']}, **ERR_MINIMALIF) + add_spender(spenders, "tapscript/minimalnotif", leaf="t6", **common, inputs=[getter("sign"), b'\x01'], failure={"inputs": [getter("sign"), b'\x0100']}, **ERR_MINIMALIF) + # Test that 1-byte public keys (which are unknown) are acceptable but nonstandard with unrelated signatures, but 0-byte public keys are not valid. + add_spender(spenders, "tapscript/unkpk/checksig", leaf="t16", standard=False, **common, **SINGLE_SIG, failure={"leaf": "t7"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/unkpk/checksigadd", leaf="t17", standard=False, **common, **SINGLE_SIG, failure={"leaf": "t10"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/unkpk/checksigverify", leaf="t18", standard=False, **common, **SINGLE_SIG, failure={"leaf": "t8"}, **ERR_UNKNOWN_PUBKEY) + # Test that 33-byte public keys (which are unknown) are acceptable but nonstandard with valid signatures, but normal pubkeys are not valid in that case. + add_spender(spenders, "tapscript/oldpk/checksig", leaf="t30", standard=False, **common, **SINGLE_SIG, sighash=bitflipper(default_sighash), failure={"leaf": "t1"}, **ERR_SIG_SCHNORR) + add_spender(spenders, "tapscript/oldpk/checksigadd", leaf="t31", standard=False, **common, **SINGLE_SIG, sighash=bitflipper(default_sighash), failure={"leaf": "t2"}, **ERR_SIG_SCHNORR) + add_spender(spenders, "tapscript/oldpk/checksigverify", leaf="t32", standard=False, **common, **SINGLE_SIG, sighash=bitflipper(default_sighash), failure={"leaf": "t28"}, **ERR_SIG_SCHNORR) + # Test that 0-byte public keys are not acceptable. + add_spender(spenders, "tapscript/emptypk/checksig", leaf="t1", **SINGLE_SIG, **common, failure={"leaf": "t7"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/emptypk/checksigverify", leaf="t2", **SINGLE_SIG, **common, failure={"leaf": "t8"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/emptypk/checksigadd", leaf="t9", **SINGLE_SIG, **common, failure={"leaf": "t10"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/emptypk/checksigadd", leaf="t35", standard=False, **SINGLE_SIG, **common, failure={"leaf": "t10"}, **ERR_UNKNOWN_PUBKEY) + # Test that OP_CHECKSIGADD results are as expected + add_spender(spenders, "tapscript/checksigaddresults", leaf="t28", **SINGLE_SIG, **common, failure={"leaf": "t27"}, err_msg="unknown error") + add_spender(spenders, "tapscript/checksigaddoversize", leaf="t29", **SINGLE_SIG, **common, failure={"leaf": "t27"}, err_msg="unknown error") + # Test that OP_CHECKSIGADD requires 3 stack elements. + add_spender(spenders, "tapscript/checksigadd3args", leaf="t9", **SINGLE_SIG, **common, failure={"leaf": "t11"}, **ERR_STACK_EMPTY) + # Test that empty signatures do not cause script failure in OP_CHECKSIG and OP_CHECKSIGADD (but do fail with empty pubkey, and do fail OP_CHECKSIGVERIFY) + add_spender(spenders, "tapscript/emptysigs/checksig", leaf="t12", **common, inputs=[b'', getter("sign")], failure={"leaf": "t13"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/emptysigs/nochecksigverify", leaf="t12", **common, inputs=[b'', getter("sign")], failure={"leaf": "t20"}, **ERR_UNKNOWN_PUBKEY) + add_spender(spenders, "tapscript/emptysigs/checksigadd", leaf="t14", **common, inputs=[b'', getter("sign")], failure={"leaf": "t15"}, **ERR_UNKNOWN_PUBKEY) + # Test that scripts over 10000 bytes (and over 201 non-push ops) are acceptable. + add_spender(spenders, "tapscript/no10000limit", leaf="t19", **SINGLE_SIG, **common) + # Test that a stack size of 1000 elements is permitted, but 1001 isn't. + add_spender(spenders, "tapscript/1000stack", leaf="t21", **SINGLE_SIG, **common, failure={"leaf": "t22"}, **ERR_STACK_SIZE) + # Test that an input stack size of 1000 elements is permitted, but 1001 isn't. + add_spender(spenders, "tapscript/1000inputs", leaf="t23", **common, inputs=[getter("sign")] + [b'' for _ in range(999)], failure={"leaf": "t24", "inputs": [getter("sign")] + [b'' for _ in range(1000)]}, **ERR_STACK_SIZE) + # Test that pushing a MAX_SCRIPT_ELEMENT_SIZE byte stack element is valid, but one longer is not. + add_spender(spenders, "tapscript/pushmaxlimit", leaf="t25", **common, **SINGLE_SIG, failure={"leaf": "t26"}, **ERR_PUSH_LIMIT) + # Test that 999-of-999 multisig works (but 1000-of-1000 triggers stack size limits) + add_spender(spenders, "tapscript/bigmulti", leaf="t33", **common, inputs=big_spend_inputs, num=999, failure={"leaf": "t34", "num": 1000}, **ERR_STACK_SIZE) + # Test that the CLEANSTACK rule is consensus critical in tapscript + add_spender(spenders, "tapscript/cleanstack", leaf="t36", tap=tap, inputs=[b'\x01'], failure={"inputs": [b'\x01', b'\x01']}, **ERR_CLEANSTACK) + + # == Test for sigops ratio limit == + + # Given a number n, and a public key pk, functions that produce a (CScript, sigops). Each script takes as + # input a valid signature with the passed pk followed by a dummy push of bytes that are to be dropped, and + # will execute sigops signature checks. + SIGOPS_RATIO_SCRIPTS = [ + # n OP_CHECKSIGVERFIYs and 1 OP_CHECKSIG. + lambda n, pk: (CScript([OP_DROP, pk] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_CHECKSIG]), n + 1), + # n OP_CHECKSIGVERIFYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIGVERIFY. + lambda n, pk: (CScript([OP_DROP, pk, OP_0, OP_IF, OP_2DUP, OP_CHECKSIGVERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_2, OP_SWAP, OP_CHECKSIGADD, OP_3, OP_EQUAL]), n + 1), + # n OP_CHECKSIGVERIFYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIG. + lambda n, pk: (CScript([random_bytes(220), OP_2DROP, pk, OP_1, OP_NOTIF, OP_2DUP, OP_CHECKSIG, OP_VERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_4, OP_SWAP, OP_CHECKSIGADD, OP_5, OP_EQUAL]), n + 1), + # n OP_CHECKSIGVERFIYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIGADD. + lambda n, pk: (CScript([OP_DROP, pk, OP_1, OP_IF, OP_ELSE, OP_2DUP, OP_6, OP_SWAP, OP_CHECKSIGADD, OP_7, OP_EQUALVERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_8, OP_SWAP, OP_CHECKSIGADD, OP_9, OP_EQUAL]), n + 1), + # n+1 OP_CHECKSIGs, but also one OP_CHECKSIG with an empty signature. + lambda n, pk: (CScript([OP_DROP, OP_0, pk, OP_CHECKSIG, OP_NOT, OP_VERIFY, pk] + [OP_2DUP, OP_CHECKSIG, OP_VERIFY] * n + [OP_CHECKSIG]), n + 1), + # n OP_CHECKSIGADDs and 1 OP_CHECKSIG, but also an OP_CHECKSIGADD with an empty signature. + lambda n, pk: (CScript([OP_DROP, OP_0, OP_10, pk, OP_CHECKSIGADD, OP_10, OP_EQUALVERIFY, pk] + [OP_2DUP, OP_16, OP_SWAP, OP_CHECKSIGADD, b'\x11', OP_EQUALVERIFY] * n + [OP_CHECKSIG]), n + 1), + ] + for annex in [None, bytes([ANNEX_TAG]) + random_bytes(random.randrange(1000))]: + for hashtype in [SIGHASH_DEFAULT, SIGHASH_ALL]: + for pubkey in [pubs[1], random_bytes(random.choice([x for x in range(2, 81) if x != 32]))]: + for fn_num, fn in enumerate(SIGOPS_RATIO_SCRIPTS): + merkledepth = random.randrange(129) + + + def predict_sigops_ratio(n, dummy_size): + """Predict whether spending fn(n, pubkey) with dummy_size will pass the ratio test.""" + script, sigops = fn(n, pubkey) + # Predict the size of the witness for a given choice of n + stacklen_size = 1 + sig_size = 64 + (hashtype != SIGHASH_DEFAULT) + siglen_size = 1 + dummylen_size = 1 + 2 * (dummy_size >= 253) + script_size = len(script) + scriptlen_size = 1 + 2 * (script_size >= 253) + control_size = 33 + 32 * merkledepth + controllen_size = 1 + 2 * (control_size >= 253) + annex_size = 0 if annex is None else len(annex) + annexlen_size = 0 if annex is None else 1 + 2 * (annex_size >= 253) + witsize = stacklen_size + sig_size + siglen_size + dummy_size + dummylen_size + script_size + scriptlen_size + control_size + controllen_size + annex_size + annexlen_size + # sigops ratio test + return witsize + 50 >= 50 * sigops + # Make sure n is high enough that with empty dummy, the script is not valid + n = 0 + while predict_sigops_ratio(n, 0): + n += 1 + # But allow picking a bit higher still + n += random.randrange(5) + # Now pick dummy size *just* large enough that the overall construction passes + dummylen = 0 + while not predict_sigops_ratio(n, dummylen): + dummylen += 1 + scripts = [("s", fn(n, pubkey)[0])] + for _ in range(merkledepth): + scripts = [scripts, random.choice(PARTNER_MERKLE_FN)] + tap = taproot_construct(pubs[0], scripts) + standard = annex is None and dummylen <= 80 and len(pubkey) == 32 + add_spender(spenders, "tapscript/sigopsratio_%i" % fn_num, tap=tap, leaf="s", annex=annex, hashtype=hashtype, key=secs[1], inputs=[getter("sign"), random_bytes(dummylen)], standard=standard, failure={"inputs": [getter("sign"), random_bytes(dummylen - 1)]}, **ERR_SIGOPS_RATIO) + + # Future leaf versions + for leafver in range(0, 0x100, 2): + if leafver == LEAF_VERSION_TAPSCRIPT or leafver == ANNEX_TAG: + # Skip the defined LEAF_VERSION_TAPSCRIPT, and the ANNEX_TAG which is not usable as leaf version + continue + scripts = [ + ("bare_c0", CScript([OP_NOP])), + ("bare_unkver", CScript([OP_NOP]), leafver), + ("return_c0", CScript([OP_RETURN])), + ("return_unkver", CScript([OP_RETURN]), leafver), + ("undecodable_c0", CScript([OP_PUSHDATA1])), + ("undecodable_unkver", CScript([OP_PUSHDATA1]), leafver), + ("bigpush_c0", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1), OP_DROP])), + ("bigpush_unkver", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1), OP_DROP]), leafver), + ("1001push_c0", CScript([OP_0] * 1001)), + ("1001push_unkver", CScript([OP_0] * 1001), leafver), + ] + random.shuffle(scripts) + tap = taproot_construct(pubs[0], scripts) + add_spender(spenders, "unkver/bare", standard=False, tap=tap, leaf="bare_unkver", failure={"leaf": "bare_c0"}, **ERR_CLEANSTACK) + add_spender(spenders, "unkver/return", standard=False, tap=tap, leaf="return_unkver", failure={"leaf": "return_c0"}, **ERR_OP_RETURN) + add_spender(spenders, "unkver/undecodable", standard=False, tap=tap, leaf="undecodable_unkver", failure={"leaf": "undecodable_c0"}, **ERR_UNDECODABLE) + add_spender(spenders, "unkver/bigpush", standard=False, tap=tap, leaf="bigpush_unkver", failure={"leaf": "bigpush_c0"}, **ERR_PUSH_LIMIT) + add_spender(spenders, "unkver/1001push", standard=False, tap=tap, leaf="1001push_unkver", failure={"leaf": "1001push_c0"}, **ERR_STACK_SIZE) + add_spender(spenders, "unkver/1001inputs", standard=False, tap=tap, leaf="bare_unkver", inputs=[b'']*1001, failure={"leaf": "bare_c0"}, **ERR_STACK_SIZE) + + # OP_SUCCESSx tests. + hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT) + for opval in range(76, 0x100): + opcode = CScriptOp(opval) + if not is_op_success(opcode): + continue + scripts = [ + ("bare_success", CScript([opcode])), + ("bare_nop", CScript([OP_NOP])), + ("unexecif_success", CScript([OP_0, OP_IF, opcode, OP_ENDIF])), + ("unexecif_nop", CScript([OP_0, OP_IF, OP_NOP, OP_ENDIF])), + ("return_success", CScript([OP_RETURN, opcode])), + ("return_nop", CScript([OP_RETURN, OP_NOP])), + ("undecodable_success", CScript([opcode, OP_PUSHDATA1])), + ("undecodable_nop", CScript([OP_NOP, OP_PUSHDATA1])), + ("undecodable_bypassed_success", CScript([OP_PUSHDATA1, OP_2, opcode])), + ("bigpush_success", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1), OP_DROP, opcode])), + ("bigpush_nop", CScript([random_bytes(MAX_SCRIPT_ELEMENT_SIZE+1), OP_DROP, OP_NOP])), + ("1001push_success", CScript([OP_0] * 1001 + [opcode])), + ("1001push_nop", CScript([OP_0] * 1001 + [OP_NOP])), + ] + random.shuffle(scripts) + tap = taproot_construct(pubs[0], scripts) + add_spender(spenders, "opsuccess/bare", standard=False, tap=tap, leaf="bare_success", failure={"leaf": "bare_nop"}, **ERR_CLEANSTACK) + add_spender(spenders, "opsuccess/unexecif", standard=False, tap=tap, leaf="unexecif_success", failure={"leaf": "unexecif_nop"}, **ERR_CLEANSTACK) + add_spender(spenders, "opsuccess/return", standard=False, tap=tap, leaf="return_success", failure={"leaf": "return_nop"}, **ERR_OP_RETURN) + add_spender(spenders, "opsuccess/undecodable", standard=False, tap=tap, leaf="undecodable_success", failure={"leaf": "undecodable_nop"}, **ERR_UNDECODABLE) + add_spender(spenders, "opsuccess/undecodable_bypass", standard=False, tap=tap, leaf="undecodable_success", failure={"leaf": "undecodable_bypassed_success"}, **ERR_UNDECODABLE) + add_spender(spenders, "opsuccess/bigpush", standard=False, tap=tap, leaf="bigpush_success", failure={"leaf": "bigpush_nop"}, **ERR_PUSH_LIMIT) + add_spender(spenders, "opsuccess/1001push", standard=False, tap=tap, leaf="1001push_success", failure={"leaf": "1001push_nop"}, **ERR_STACK_SIZE) + add_spender(spenders, "opsuccess/1001inputs", standard=False, tap=tap, leaf="bare_success", inputs=[b'']*1001, failure={"leaf": "bare_nop"}, **ERR_STACK_SIZE) + + # Non-OP_SUCCESSx (verify that those aren't accidentally treated as OP_SUCCESSx) + for opval in range(0, 0x100): + opcode = CScriptOp(opval) + if is_op_success(opcode): + continue + scripts = [ + ("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)), + ("op_success", CScript([OP_RETURN, CScriptOp(0x50)])) + ] + tap = taproot_construct(pubs[0], scripts) + add_spender(spenders, "alwaysvalid/notsuccessx", tap=tap, leaf="op_success", inputs=[], standard=False, failure={"leaf": "normal"}) # err_msg differs based on opcode + + # == Legacy tests == + + # Also add a few legacy spends into the mix, so that transactions which combine taproot and pre-taproot spends get tested too. + for compressed in [False, True]: + eckey1 = ECKey() + eckey1.set(generate_privkey(), compressed) + pubkey1 = eckey1.get_pubkey().get_bytes() + eckey2 = ECKey() + eckey2.set(generate_privkey(), compressed) + for p2sh in [False, True]: + 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/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. + for p2sh in [False, True]: + 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 (p2sh or witv0) + add_spender(spenders, "compat/nocsa", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=CScript([OP_IF, OP_11, pubkey1, OP_CHECKSIGADD, OP_12, OP_EQUAL, OP_ELSE, pubkey1, OP_CHECKSIG, OP_ENDIF]), key=eckey1, sigops_weight=4-3*witv0, inputs=[getter("sign"), b''], failure={"inputs": [getter("sign"), b'\x01']}, **ERR_UNDECODABLE) + + return spenders + +def spenders_taproot_inactive(): + """Spenders for testing that pre-activation Taproot rules don't apply.""" + + spenders = [] + + sec = generate_privkey() + pub, _ = compute_xonly_pubkey(sec) + scripts = [ + ("pk", CScript([pub, OP_CHECKSIG])), + ("future_leaf", CScript([pub, OP_CHECKSIG]), 0xc2), + ("op_success", CScript([pub, OP_CHECKSIG, OP_0, OP_IF, CScriptOp(0x50), OP_ENDIF])), + ] + tap = taproot_construct(pub, scripts) + + # Test that keypath spending is valid & non-standard, regardless of validity. + add_spender(spenders, "inactive/keypath_valid", key=sec, tap=tap, standard=False) + add_spender(spenders, "inactive/keypath_invalidsig", key=sec, tap=tap, standard=False, sighash=bitflipper(default_sighash)) + add_spender(spenders, "inactive/keypath_empty", key=sec, tap=tap, standard=False, witness=[]) + + # Same for scriptpath spending (and features like annex, leaf versions, or OP_SUCCESS don't change this) + add_spender(spenders, "inactive/scriptpath_valid", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")]) + add_spender(spenders, "inactive/scriptpath_invalidsig", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], sighash=bitflipper(default_sighash)) + add_spender(spenders, "inactive/scriptpath_invalidcb", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], controlblock=bitflipper(default_controlblock)) + add_spender(spenders, "inactive/scriptpath_valid_unkleaf", key=sec, tap=tap, leaf="future_leaf", standard=False, inputs=[getter("sign")]) + add_spender(spenders, "inactive/scriptpath_invalid_unkleaf", key=sec, tap=tap, leaf="future_leaf", standard=False, inputs=[getter("sign")], sighash=bitflipper(default_sighash)) + add_spender(spenders, "inactive/scriptpath_valid_opsuccess", key=sec, tap=tap, leaf="op_success", standard=False, inputs=[getter("sign")]) + add_spender(spenders, "inactive/scriptpath_valid_opsuccess", key=sec, tap=tap, leaf="op_success", standard=False, inputs=[getter("sign")], sighash=bitflipper(default_sighash)) + + return spenders + +# Consensus validation flags to use in dumps for tests with "legacy/" or "inactive/" prefix. +LEGACY_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY" +# Consensus validation flags to use in dumps for all other tests. +TAPROOT_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY,TAPROOT" + +def dump_json_test(tx, input_utxos, idx, success, failure): + spender = input_utxos[idx].spender + # Determine flags to dump + flags = LEGACY_FLAGS if spender.comment.startswith("legacy/") or spender.comment.startswith("inactive/") else TAPROOT_FLAGS + + fields = [ + ("tx", tx.serialize().hex()), + ("prevouts", [x.output.serialize().hex() for x in input_utxos]), + ("index", idx), + ("flags", flags), + ("comment", spender.comment) + ] + + # The "final" field indicates that a spend should be always valid, even with more validation flags enabled + # than the listed ones. Use standardness as a proxy for this (which gives a conservative underestimate). + if spender.is_standard: + fields.append(("final", True)) + + def dump_witness(wit): + return OrderedDict([("scriptSig", wit[0].hex()), ("witness", [x.hex() for x in wit[1]])]) + if success is not None: + fields.append(("success", dump_witness(success))) + if failure is not None: + fields.append(("failure", dump_witness(failure))) + + # Write the dump to $TEST_DUMP_DIR/x/xyz... where x,y,z,... are the SHA1 sum of the dump (which makes the + # file naming scheme compatible with fuzzing infrastructure). + dump = json.dumps(OrderedDict(fields)) + ",\n" + sha1 = hashlib.sha1(dump.encode("utf-8")).hexdigest() + dirname = os.environ.get("TEST_DUMP_DIR", ".") + ("/%s" % sha1[0]) + os.makedirs(dirname, exist_ok=True) + with open(dirname + ("/%s" % sha1), 'w', encoding="utf8") as f: + f.write(dump) + +# Data type to keep track of UTXOs, where they were created, and how to spend them. +UTXOData = namedtuple('UTXOData', 'outpoint,output,spender') + +class TaprootTest(BitcoinTestFramework): + def add_options(self, parser): + parser.add_argument("--dumptests", dest="dump_tests", default=False, action="store_true", + help="Dump generated test cases to directory set by TEST_DUMP_DIR environment variable") + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + # Node 0 has Taproot inactive, Node 1 active. + self.extra_args = [["-par=1", "-vbparams=taproot:1:1"], ["-par=1"]] + + def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False): + + # Deplete block of any non-tapscript sigops using a single additional 0-value coinbase output. + # It is not impossible to fit enough tapscript sigops to hit the old 80k limit without + # busting txin-level limits. We simply have to account for the p2pk outputs in all + # transactions. + extra_output_script = CScript([OP_CHECKSIG]*((MAX_BLOCK_SIGOPS_WEIGHT - sigops_weight) // WITNESS_SCALE_FACTOR)) + + block = create_block(self.tip, create_coinbase(self.lastblockheight + 1, pubkey=cb_pubkey, extra_output_script=extra_output_script, fees=fees), self.lastblocktime + 1) + block.nVersion = 4 + for tx in txs: + tx.rehash() + block.vtx.append(tx) + block.hashMerkleRoot = block.calc_merkle_root() + witness and add_witness_commitment(block) + block.rehash() + block.solve() + block_response = node.submitblock(block.serialize().hex()) + if err_msg is not None: + assert block_response is not None and err_msg in block_response, "Missing error message '%s' from block response '%s': %s" % (err_msg, "(None)" if block_response is None else block_response, msg) + if (accept): + assert node.getbestblockhash() == block.hash, "Failed to accept: %s (response: %s)" % (msg, block_response) + self.tip = block.sha256 + self.lastblockhash = block.hash + self.lastblocktime += 1 + self.lastblockheight += 1 + else: + assert node.getbestblockhash() == self.lastblockhash, "Failed to reject: " + msg + + def test_spenders(self, node, spenders, input_counts): + """Run randomized tests with a number of "spenders". + + Steps: + 1) Generate an appropriate UTXO for each spender to test spend conditions + 2) Generate 100 random addresses of all wallet types: pkh/sh_wpkh/wpkh + 3) Select random number of inputs from (1) + 4) Select random number of addresses from (2) as outputs + + Each spender embodies a test; in a large randomized test, it is verified + that toggling the valid argument to each lambda toggles the validity of + the transaction. This is accomplished by constructing transactions consisting + of all valid inputs, except one invalid one. + """ + + # Construct a bunch of sPKs that send coins back to the host wallet + self.log.info("- Constructing addresses for returning coins") + host_spks = [] + host_pubkeys = [] + for i in range(16): + addr = node.getnewaddress(address_type=random.choice(["legacy", "p2sh-segwit", "bech32"])) + info = node.getaddressinfo(addr) + spk = bytes.fromhex(info['scriptPubKey']) + host_spks.append(spk) + host_pubkeys.append(bytes.fromhex(info['pubkey'])) + + # Initialize variables used by block_submit(). + self.lastblockhash = node.getbestblockhash() + self.tip = int(self.lastblockhash, 16) + block = node.getblock(self.lastblockhash) + self.lastblockheight = block['height'] + self.lastblocktime = block['time'] + + # Create transactions spending up to 50 of the wallet's inputs, with one output for each spender, and + # one change output at the end. The transaction is constructed on the Python side to enable + # having multiple outputs to the same address and outputs with no assigned address. The wallet + # is then asked to sign it through signrawtransactionwithwallet, and then added to a block on the + # Python side (to bypass standardness rules). + self.log.info("- Creating test UTXOs...") + random.shuffle(spenders) + normal_utxos = [] + mismatching_utxos = [] # UTXOs with input that requires mismatching output position + done = 0 + while done < len(spenders): + # Compute how many UTXOs to create with this transaction + count_this_tx = min(len(spenders) - done, (len(spenders) + 4) // 5, 10000) + + fund_tx = CTransaction() + # Add the 50 highest-value inputs + unspents = node.listunspent() + random.shuffle(unspents) + unspents.sort(key=lambda x: int(x["amount"] * 100000000), reverse=True) + if len(unspents) > 50: + unspents = unspents[:50] + random.shuffle(unspents) + balance = 0 + for unspent in unspents: + balance += int(unspent["amount"] * 100000000) + txid = int(unspent["txid"], 16) + fund_tx.vin.append(CTxIn(COutPoint(txid, int(unspent["vout"])), CScript())) + # Add outputs + cur_progress = done / len(spenders) + next_progress = (done + count_this_tx) / len(spenders) + change_goal = (1.0 - 0.6 * next_progress) / (1.0 - 0.6 * cur_progress) * balance + self.log.debug("Create %i UTXOs in a transaction spending %i inputs worth %.8f (sending ~%.8f to change)" % (count_this_tx, len(unspents), balance * 0.00000001, change_goal * 0.00000001)) + for i in range(count_this_tx): + avg = (balance - change_goal) / (count_this_tx - i) + amount = int(random.randrange(int(avg*0.85 + 0.5), int(avg*1.15 + 0.5)) + 0.5) + balance -= amount + fund_tx.vout.append(CTxOut(amount, spenders[done + i].script)) + # Add change + fund_tx.vout.append(CTxOut(balance - 10000, random.choice(host_spks))) + # Ask the wallet to sign + ss = BytesIO(bytes.fromhex(node.signrawtransactionwithwallet(ToHex(fund_tx))["hex"])) + fund_tx.deserialize(ss) + # Construct UTXOData entries + fund_tx.rehash() + for i in range(count_this_tx): + utxodata = UTXOData(outpoint=COutPoint(fund_tx.sha256, i), output=fund_tx.vout[i], spender=spenders[done]) + if utxodata.spender.need_vin_vout_mismatch: + mismatching_utxos.append(utxodata) + else: + normal_utxos.append(utxodata) + done += 1 + # Mine into a block + self.block_submit(node, [fund_tx], "Funding tx", None, random.choice(host_pubkeys), 10000, MAX_BLOCK_SIGOPS_WEIGHT, True, True) + + # Consume groups of choice(input_coins) from utxos in a tx, testing the spenders. + self.log.info("- Running %i spending tests" % done) + random.shuffle(normal_utxos) + random.shuffle(mismatching_utxos) + assert done == len(normal_utxos) + len(mismatching_utxos) + + left = done + while left: + # Construct CTransaction with random nVersion, nLocktime + tx = CTransaction() + tx.nVersion = random.choice([1, 2, random.randint(-0x80000000, 0x7fffffff)]) + min_sequence = (tx.nVersion != 1 and tx.nVersion != 0) * 0x80000000 # The minimum sequence number to disable relative locktime + if random.choice([True, False]): + tx.nLockTime = random.randrange(LOCKTIME_THRESHOLD, self.lastblocktime - 7200) # all absolute locktimes in the past + else: + tx.nLockTime = random.randrange(self.lastblockheight + 1) # all block heights in the past + + # Decide how many UTXOs to test with. + acceptable = [n for n in input_counts if n <= left and (left - n > max(input_counts) or (left - n) in [0] + input_counts)] + num_inputs = random.choice(acceptable) + + # If we have UTXOs that require mismatching inputs/outputs left, include exactly one of those + # unless there is only one normal UTXO left (as tests with mismatching UTXOs require at least one + # normal UTXO to go in the first position), and we don't want to run out of normal UTXOs. + input_utxos = [] + while len(mismatching_utxos) and (len(input_utxos) == 0 or len(normal_utxos) == 1): + input_utxos.append(mismatching_utxos.pop()) + left -= 1 + + # Top up until we hit num_inputs (but include at least one normal UTXO always). + for _ in range(max(1, num_inputs - len(input_utxos))): + input_utxos.append(normal_utxos.pop()) + left -= 1 + + # The first input cannot require a mismatching output (as there is at least one output). + while True: + random.shuffle(input_utxos) + if not input_utxos[0].spender.need_vin_vout_mismatch: + break + first_mismatch_input = None + for i in range(len(input_utxos)): + if input_utxos[i].spender.need_vin_vout_mismatch: + first_mismatch_input = i + assert first_mismatch_input is None or first_mismatch_input > 0 + + # Decide fee, and add CTxIns to tx. + amount = sum(utxo.output.nValue for utxo in input_utxos) + fee = min(random.randrange(MIN_FEE * 2, MIN_FEE * 4), amount - DUST_LIMIT) # 10000-20000 sat fee + in_value = amount - fee + tx.vin = [CTxIn(outpoint=utxo.outpoint, nSequence=random.randint(min_sequence, 0xffffffff)) for utxo in input_utxos] + tx.wit.vtxinwit = [CTxInWitness() for _ in range(len(input_utxos))] + sigops_weight = sum(utxo.spender.sigops_weight for utxo in input_utxos) + self.log.debug("Test: %s" % (", ".join(utxo.spender.comment for utxo in input_utxos))) + + # Add 1 to 4 random outputs (but constrained by inputs that require mismatching outputs) + num_outputs = random.choice(range(1, 1 + min(4, 4 if first_mismatch_input is None else first_mismatch_input))) + assert in_value >= 0 and fee - num_outputs * DUST_LIMIT >= MIN_FEE + for i in range(num_outputs): + tx.vout.append(CTxOut()) + if in_value <= DUST_LIMIT: + tx.vout[-1].nValue = DUST_LIMIT + elif i < num_outputs - 1: + tx.vout[-1].nValue = in_value + else: + tx.vout[-1].nValue = random.randint(DUST_LIMIT, in_value) + in_value -= tx.vout[-1].nValue + tx.vout[-1].scriptPubKey = random.choice(host_spks) + sigops_weight += CScript(tx.vout[-1].scriptPubKey).GetSigOpCount(False) * WITNESS_SCALE_FACTOR + fee += in_value + assert fee >= 0 + + # Select coinbase pubkey + cb_pubkey = random.choice(host_pubkeys) + sigops_weight += 1 * WITNESS_SCALE_FACTOR + + # Precompute one satisfying and one failing scriptSig/witness for each input. + input_data = [] + for i in range(len(input_utxos)): + fn = input_utxos[i].spender.sat_function + fail = None + success = fn(tx, i, [utxo.output for utxo in input_utxos], True) + if not input_utxos[i].spender.no_fail: + fail = fn(tx, i, [utxo.output for utxo in input_utxos], False) + input_data.append((fail, success)) + if self.options.dump_tests: + dump_json_test(tx, input_utxos, i, success, fail) + + # Sign each input incorrectly once on each complete signing pass, except the very last. + for fail_input in list(range(len(input_utxos))) + [None]: + # Skip trying to fail at spending something that can't be made to fail. + if fail_input is not None and input_utxos[fail_input].spender.no_fail: + continue + # Expected message with each input failure, may be None(which is ignored) + expected_fail_msg = None if fail_input is None else input_utxos[fail_input].spender.err_msg + # Fill inputs/witnesses + for i in range(len(input_utxos)): + tx.vin[i].scriptSig = input_data[i][i != fail_input][0] + tx.wit.vtxinwit[i].scriptWitness.stack = input_data[i][i != fail_input][1] + # Submit to mempool to check standardness + is_standard_tx = fail_input is None and all(utxo.spender.is_standard for utxo in input_utxos) and tx.nVersion >= 1 and tx.nVersion <= 2 + tx.rehash() + msg = ','.join(utxo.spender.comment + ("*" if n == fail_input else "") for n, utxo in enumerate(input_utxos)) + if is_standard_tx: + node.sendrawtransaction(tx.serialize().hex(), 0) + assert node.getmempoolentry(tx.hash) is not None, "Failed to accept into mempool: " + msg + else: + assert_raises_rpc_error(-26, None, node.sendrawtransaction, tx.serialize().hex(), 0) + # Submit in a block + self.block_submit(node, [tx], msg, witness=True, accept=fail_input is None, cb_pubkey=cb_pubkey, fees=fee, sigops_weight=sigops_weight, err_msg=expected_fail_msg) + + if (len(spenders) - left) // 200 > (len(spenders) - left - len(input_utxos)) // 200: + self.log.info(" - %i tests done" % (len(spenders) - left)) + + assert left == 0 + assert len(normal_utxos) == 0 + assert len(mismatching_utxos) == 0 + self.log.info(" - Done") + + def run_test(self): + # Post-taproot activation tests go first (pre-taproot tests' blocks are invalid post-taproot). + self.log.info("Post-activation tests...") + self.nodes[1].generate(101) + self.test_spenders(self.nodes[1], spenders_taproot_active(), input_counts=[1, 2, 2, 2, 2, 3]) + + # Re-connect nodes in case they have been disconnected + self.disconnect_nodes(0, 1) + self.connect_nodes(0, 1) + + # Transfer value of the largest 500 coins to pre-taproot node. + addr = self.nodes[0].getnewaddress() + + unsp = self.nodes[1].listunspent() + unsp = sorted(unsp, key=lambda i: i['amount'], reverse=True) + unsp = unsp[:500] + + rawtx = self.nodes[1].createrawtransaction( + inputs=[{ + 'txid': i['txid'], + 'vout': i['vout'] + } for i in unsp], + outputs={addr: sum(i['amount'] for i in unsp)} + ) + rawtx = self.nodes[1].signrawtransactionwithwallet(rawtx)['hex'] + + # Mine a block with the transaction + block = create_block(tmpl=self.nodes[1].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS), txlist=[rawtx]) + add_witness_commitment(block) + block.rehash() + block.solve() + assert_equal(None, self.nodes[1].submitblock(block.serialize().hex())) + self.sync_blocks() + + # Pre-taproot activation tests. + self.log.info("Pre-activation tests...") + # Run each test twice; once in isolation, and once combined with others. Testing in isolation + # means that the standardness is verified in every test (as combined transactions are only standard + # when all their inputs are standard). + self.test_spenders(self.nodes[0], spenders_taproot_inactive(), input_counts=[1]) + self.test_spenders(self.nodes[0], spenders_taproot_inactive(), input_counts=[2, 3]) + + +if __name__ == '__main__': + TaprootTest().main() diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 81c007c27b..1257dff1ae 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -95,7 +95,7 @@ class TestBitcoinCli(BitcoinTestFramework): assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) # Setup to test -getinfo, -generate, and -rpcwallet= with multiple wallets. - wallets = ['', 'Encrypted', 'secret'] + wallets = [self.default_wallet_name, 'Encrypted', 'secret'] amounts = [BALANCE + Decimal('9.999928'), Decimal(9), Decimal(31)] self.nodes[0].createwallet(wallet_name=wallets[1]) self.nodes[0].createwallet(wallet_name=wallets[2]) diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 17032a3b83..d675ae174c 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -11,7 +11,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import CTransaction, hash256, FromHex from test_framework.util import ( assert_equal, - connect_nodes, assert_raises_rpc_error, ) from io import BytesIO @@ -75,6 +74,7 @@ class ZMQTest (BitcoinTestFramework): self.test_sequence() self.test_mempool_sync() self.test_reorg() + self.test_multiple_interfaces() finally: # Destroy the ZMQ context. self.log.debug("Destroying ZMQ context") @@ -101,7 +101,7 @@ class ZMQTest (BitcoinTestFramework): rawtx = subs[3] self.restart_node(0, ["-zmqpub%s=%s" % (sub.topic.decode(), address) for sub in [hashblock, hashtx, rawblock, rawtx]]) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) for socket in sockets: socket.connect(address) @@ -206,7 +206,7 @@ class ZMQTest (BitcoinTestFramework): connect_blocks = self.nodes[1].generatetoaddress(2, ADDRESS_BCRT1_P2WSH_OP_TRUE) # nodes[0] will reorg chain after connecting back nodes[1] - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks() # tx in mempool valid but not advertised # Should receive nodes[1] tip @@ -263,7 +263,7 @@ class ZMQTest (BitcoinTestFramework): self.nodes[1].generatetoaddress(2, ADDRESS_BCRT1_P2WSH_OP_TRUE) # nodes[0] will reorg chain after connecting back nodes[1] - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) # Then we receive all block (dis)connect notifications for the 2 block reorg assert_equal((dc_block, "D", None), seq.receive_sequence()) @@ -405,7 +405,7 @@ class ZMQTest (BitcoinTestFramework): seq = ZMQSubscriber(socket, b'sequence') self.restart_node(0, ['-zmqpub%s=%s' % (seq.topic.decode(), address)]) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) socket.connect(address) # Relax so that the subscriber is ready before publishing zmq messages sleep(0.2) @@ -506,5 +506,28 @@ class ZMQTest (BitcoinTestFramework): self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + def test_multiple_interfaces(self): + # Set up two subscribers with different addresses + subscribers = [] + for i in range(2): + address = 'tcp://127.0.0.1:%d' % (28334 + i) + socket = self.ctx.socket(zmq.SUB) + socket.set(zmq.RCVTIMEO, 60000) + hashblock = ZMQSubscriber(socket, b"hashblock") + socket.connect(address) + subscribers.append({'address': address, 'hashblock': hashblock}) + + self.restart_node(0, ['-zmqpub%s=%s' % (subscriber['hashblock'].topic.decode(), subscriber['address']) for subscriber in subscribers]) + + # Relax so that the subscriber is ready before publishing zmq messages + sleep(0.2) + + # Generate 1 block in nodes[0] and receive all notifications + self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + + # Should receive the same block hash on both subscribers + assert_equal(self.nodes[0].getbestblockhash(), subscribers[0]['hashblock'].receive().hex()) + assert_equal(self.nodes[0].getbestblockhash(), subscribers[1]['hashblock'].receive().hex()) + if __name__ == '__main__': ZMQTest().main() diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 57a059b7f7..fc4c775668 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -83,31 +83,31 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction not in the mempool') - fee = 0.00000700 + fee = Decimal('0.000007') raw_tx_0 = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{"txid": txid_in_block, "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}], # RBF is used later - outputs=[{node.getnewaddress(): 0.3 - fee}], + outputs=[{node.getnewaddress(): Decimal('0.3') - fee}], ))['hex'] tx = CTransaction() tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) txid_0 = tx.rehash() self.check_mempool_result( - result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': Decimal(str(fee))}}], + result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee}}], rawtxs=[raw_tx_0], ) self.log.info('A final transaction not in the mempool') coin = coins.pop() # Pick a random coin(base) to spend - output_amount = 0.025 + output_amount = Decimal('0.025') raw_tx_final = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{'txid': coin['txid'], 'vout': coin['vout'], "sequence": 0xffffffff}], # SEQUENCE_FINAL outputs=[{node.getnewaddress(): output_amount}], locktime=node.getblockcount() + 2000, # Can be anything ))['hex'] tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_final))) - fee_expected = int(coin['amount']) - output_amount + fee_expected = coin['amount'] - output_amount self.check_mempool_result( - result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': Decimal(str(fee_expected))}}], + result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee_expected}}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) @@ -130,7 +130,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) txid_0 = tx.rehash() self.check_mempool_result( - result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': Decimal(str(2 * fee))}}], + result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': (2 * fee)}}], rawtxs=[raw_tx_0], ) @@ -190,7 +190,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) # Reference tx should be valid on itself self.check_mempool_result( - result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal(str(0.1 - 0.05))}}], + result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.1') - Decimal('0.05')}}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) diff --git a/test/functional/mempool_compatibility.py b/test/functional/mempool_compatibility.py index fd3dd47e2d..8ac91bd008 100755 --- a/test/functional/mempool_compatibility.py +++ b/test/functional/mempool_compatibility.py @@ -21,6 +21,7 @@ from test_framework.test_framework import BitcoinTestFramework class MempoolCompatibilityTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 + self.wallet_names = [None, self.default_wallet_name] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -28,10 +29,10 @@ class MempoolCompatibilityTest(BitcoinTestFramework): def setup_network(self): self.add_nodes(self.num_nodes, versions=[ - 150200, # oldest version supported by the test framework + 190100, # oldest version with getmempoolinfo.loaded (used to avoid intermittent issues) None, ]) - self.start_nodes([[], ["-wallet="]]) + self.start_nodes() self.import_deterministic_coinbase_privkeys() def run_test(self): @@ -71,5 +72,6 @@ class MempoolCompatibilityTest(BitcoinTestFramework): assert old_tx_hash in old_node.getrawmempool() assert unbroadcasted_tx_hash in old_node.getrawmempool() + if __name__ == "__main__": MempoolCompatibilityTest().main() diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index f73f1a02a2..70cd4ebb3b 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -45,8 +45,6 @@ from test_framework.util import ( assert_equal, assert_greater_than_or_equal, assert_raises_rpc_error, - connect_nodes, - disconnect_nodes, ) @@ -83,11 +81,11 @@ class MempoolPersistTest(BitcoinTestFramework): assert_greater_than_or_equal(tx_creation_time_higher, tx_creation_time) # disconnect nodes & make a txn that remains in the unbroadcast set. - disconnect_nodes(self.nodes[0], 1) + 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")) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) 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() diff --git a/test/functional/mempool_spend_coinbase.py b/test/functional/mempool_spend_coinbase.py index 854d506f0d..86d382ff69 100755 --- a/test/functional/mempool_spend_coinbase.py +++ b/test/functional/mempool_spend_coinbase.py @@ -13,44 +13,48 @@ but less mature coinbase spends are NOT. """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.blocktools import create_raw_transaction from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.wallet import MiniWallet class MempoolSpendCoinbaseTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + self.setup_clean_chain = True def run_test(self): + wallet = MiniWallet(self.nodes[0]) + + wallet.generate(200) chain_height = self.nodes[0].getblockcount() assert_equal(chain_height, 200) - node0_address = self.nodes[0].getnewaddress() # Coinbase at height chain_height-100+1 ok in mempool, should # get mined. Coinbase at height chain_height-100+2 is - # is too immature to spend. + # too immature to spend. b = [self.nodes[0].getblockhash(n) for n in range(101, 103)] coinbase_txids = [self.nodes[0].getblock(h)['tx'][0] for h in b] - spends_raw = [create_raw_transaction(self.nodes[0], txid, node0_address, amount=49.99) for txid in coinbase_txids] + utxo_101 = wallet.get_utxo(txid=coinbase_txids[0]) + utxo_102 = wallet.get_utxo(txid=coinbase_txids[1]) - spend_101_id = self.nodes[0].sendrawtransaction(spends_raw[0]) + spend_101_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_101)["txid"] # coinbase at height 102 should be too immature to spend - assert_raises_rpc_error(-26,"bad-txns-premature-spend-of-coinbase", self.nodes[0].sendrawtransaction, spends_raw[1]) + assert_raises_rpc_error(-26, + "bad-txns-premature-spend-of-coinbase", + lambda: wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_102)) # mempool should have just spend_101: - assert_equal(self.nodes[0].getrawmempool(), [ spend_101_id ]) + assert_equal(self.nodes[0].getrawmempool(), [spend_101_id]) # mine a block, spend_101 should get confirmed self.nodes[0].generate(1) assert_equal(set(self.nodes[0].getrawmempool()), set()) # ... and now height 102 can be spent: - spend_102_id = self.nodes[0].sendrawtransaction(spends_raw[1]) - assert_equal(self.nodes[0].getrawmempool(), [ spend_102_id ]) + spend_102_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_102)["txid"] + assert_equal(self.nodes[0].getrawmempool(), [spend_102_id]) + if __name__ == '__main__': MempoolSpendCoinbaseTest().main() diff --git a/test/functional/mempool_unbroadcast.py b/test/functional/mempool_unbroadcast.py index abd5a03d95..b475b65e68 100755 --- a/test/functional/mempool_unbroadcast.py +++ b/test/functional/mempool_unbroadcast.py @@ -11,9 +11,7 @@ from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, create_confirmed_utxos, - disconnect_nodes, ) MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # 15 minutes in seconds @@ -36,7 +34,7 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): min_relay_fee = node.getnetworkinfo()["relayfee"] utxos = create_confirmed_utxos(min_relay_fee, node, 10) - disconnect_nodes(node, 1) + self.disconnect_nodes(0, 1) self.log.info("Generate transactions that only node 0 knows about") @@ -70,7 +68,7 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): self.restart_node(0) self.log.info("Reconnect nodes & check if they are sent to node 1") - connect_nodes(node, 1) + self.connect_nodes(0, 1) # fast forward into the future & ensure that the second node has the txns node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY) @@ -91,7 +89,7 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): time.sleep(2) # allow sufficient time for possibility of broadcast assert_equal(len(conn.get_invs()), 0) - disconnect_nodes(node, 1) + self.disconnect_nodes(0, 1) node.disconnect_p2ps() def test_txn_removal(self): diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 1b2c7644bd..ba467c1517 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -13,6 +13,7 @@ from decimal import Decimal from test_framework.blocktools import ( create_coinbase, + NORMAL_GBT_REQUEST_PARAMS, TIME_GENESIS_BLOCK, ) from test_framework.messages import ( @@ -25,9 +26,11 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, ) +VERSIONBITS_TOP_BITS = 0x20000000 +VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 + def assert_template(node, block, expect, rehash=True): if rehash: @@ -55,8 +58,16 @@ class MiningTest(BitcoinTestFramework): assert_equal(mining_info['blocks'], 200) assert_equal(mining_info['currentblocktx'], 0) assert_equal(mining_info['currentblockweight'], 4000) + + self.log.info('test blockversion') + self.restart_node(0, extra_args=['-mocktime={}'.format(t), '-blockversion=1337']) + self.connect_nodes(0, 1) + assert_equal(1337, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) + self.restart_node(0, extra_args=['-mocktime={}'.format(t)]) + self.connect_nodes(0, 1) + assert_equal(VERSIONBITS_TOP_BITS + (1 << VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT), self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) self.restart_node(0) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) def run_test(self): self.mine_chain() @@ -80,7 +91,7 @@ class MiningTest(BitcoinTestFramework): # Mine a block to leave initial block download node.generatetoaddress(1, node.get_deterministic_priv_key().address) - tmpl = node.getblocktemplate({'rules': ['segwit']}) + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) self.log.info("getblocktemplate: Test capability advertised") assert 'proposal' in tmpl['capabilities'] assert 'coinbasetxn' not in tmpl diff --git a/test/functional/mining_getblocktemplate_longpoll.py b/test/functional/mining_getblocktemplate_longpoll.py index 6d0b241e57..2adafb1fdb 100755 --- a/test/functional/mining_getblocktemplate_longpoll.py +++ b/test/functional/mining_getblocktemplate_longpoll.py @@ -5,11 +5,13 @@ """Test longpolling with getblocktemplate.""" from decimal import Decimal +import random +import threading from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import get_rpc_proxy, random_transaction +from test_framework.util import get_rpc_proxy +from test_framework.wallet import MiniWallet -import threading class LongpollThread(threading.Thread): def __init__(self, node): @@ -29,45 +31,48 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): self.num_nodes = 2 self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): self.log.info("Warning: this test will take about 70 seconds in the best case. Be patient.") + self.log.info("Test that longpollid doesn't change between successive getblocktemplate() invocations if nothing else happens") self.nodes[0].generate(10) template = self.nodes[0].getblocktemplate({'rules': ['segwit']}) longpollid = template['longpollid'] - # longpollid should not change between successive invocations if nothing else happens template2 = self.nodes[0].getblocktemplate({'rules': ['segwit']}) assert template2['longpollid'] == longpollid - # Test 1: test that the longpolling wait if we do nothing + self.log.info("Test that longpoll waits if we do nothing") thr = LongpollThread(self.nodes[0]) thr.start() # check that thread still lives thr.join(5) # wait 5 seconds or until thread exits assert thr.is_alive() - # Test 2: test that longpoll will terminate if another node generates a block - self.nodes[1].generate(1) # generate a block on another node + miniwallets = [ MiniWallet(node) for node in self.nodes ] + self.log.info("Test that longpoll will terminate if another node generates a block") + miniwallets[1].generate(1) # generate a block on another node # check that thread will exit now that new transaction entered mempool thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() - # Test 3: test that longpoll will terminate if we generate a block ourselves + self.log.info("Test that longpoll will terminate if we generate a block ourselves") thr = LongpollThread(self.nodes[0]) thr.start() - self.nodes[0].generate(1) # generate a block on another node + miniwallets[0].generate(1) # generate a block on own node thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() - # Test 4: test that introducing a new transaction into the mempool will terminate the longpoll + # Add enough mature utxos to the wallets, so that all txs spend confirmed coins + self.nodes[0].generate(100) + self.sync_blocks() + + self.log.info("Test that introducing a new transaction into the mempool will terminate the longpoll") thr = LongpollThread(self.nodes[0]) thr.start() # generate a random transaction and submit it min_relay_fee = self.nodes[0].getnetworkinfo()["relayfee"] - # min_relay_fee is fee per 1000 bytes, which should be more than enough. - (txid, txhex, fee) = random_transaction(self.nodes, Decimal("1.1"), min_relay_fee, Decimal("0.001"), 20) + fee_rate = min_relay_fee + Decimal('0.00000010') * random.randint(0,20) + miniwallets[0].send_self_transfer(from_node=random.choice(self.nodes), + fee_rate=fee_rate) # after one minute, every 10 seconds the mempool is probed, so in 80 seconds it should have returned thr.join(60 + 20) assert not thr.is_alive() diff --git a/test/functional/p2p_addrv2_relay.py b/test/functional/p2p_addrv2_relay.py new file mode 100755 index 0000000000..23ce3e5d04 --- /dev/null +++ b/test/functional/p2p_addrv2_relay.py @@ -0,0 +1,79 @@ +#!/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 addrv2 relay +""" + +import time + +from test_framework.messages import ( + CAddress, + msg_addrv2, + NODE_NETWORK, + NODE_WITNESS, +) +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +ADDRS = [] +for i in range(10): + addr = CAddress() + addr.time = int(time.time()) + i + addr.nServices = NODE_NETWORK | NODE_WITNESS + addr.ip = "123.123.123.{}".format(i % 256) + addr.port = 8333 + i + ADDRS.append(addr) + + +class AddrReceiver(P2PInterface): + addrv2_received_and_checked = False + + def __init__(self): + super().__init__(support_addrv2 = True) + + def on_addrv2(self, message): + for addr in message.addrs: + assert_equal(addr.nServices, 9) + assert addr.ip.startswith('123.123.123.') + assert (8333 <= addr.port < 8343) + self.addrv2_received_and_checked = True + + def wait_for_addrv2(self): + self.wait_until(lambda: "addrv2" in self.last_message) + + +class AddrTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def run_test(self): + self.log.info('Create connection that sends addrv2 messages') + addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) + msg = msg_addrv2() + + self.log.info('Send too-large addrv2 message') + msg.addrs = ADDRS * 101 + with self.nodes[0].assert_debug_log(['addrv2 message size = 1010']): + addr_source.send_and_ping(msg) + + self.log.info('Check that addrv2 message content is relayed and added to addrman') + addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver()) + msg.addrs = ADDRS + with self.nodes[0].assert_debug_log([ + 'Added 10 addresses from 127.0.0.1: 0 tried', + 'received: addrv2 (131 bytes) peer=0', + 'sending addrv2 (131 bytes) peer=1', + ]): + addr_source.send_and_ping(msg) + self.nodes[0].setmocktime(int(time.time()) + 30 * 60) + addr_receiver.wait_for_addrv2() + + assert addr_receiver.addrv2_received_and_checked + + +if __name__ == '__main__': + AddrTest().main() diff --git a/test/functional/p2p_blockfilters.py b/test/functional/p2p_blockfilters.py index 84178d0dd7..3250cbecf9 100755 --- a/test/functional/p2p_blockfilters.py +++ b/test/functional/p2p_blockfilters.py @@ -22,8 +22,6 @@ from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, ) class CFiltersClient(P2PInterface): @@ -61,7 +59,7 @@ class CompactFiltersTest(BitcoinTestFramework): self.sync_blocks(timeout=600) # Stale blocks by disconnecting nodes 0 & 1, mining, then reconnecting - disconnect_nodes(self.nodes[0], 1) + self.disconnect_nodes(0, 1) self.nodes[0].generate(1) self.wait_until(lambda: self.nodes[0].getblockcount() == 1000) @@ -90,7 +88,7 @@ class CompactFiltersTest(BitcoinTestFramework): assert_equal(len(response.headers), 1) self.log.info("Reorg node 0 to a new chain.") - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks(timeout=600) main_block_hash = self.nodes[0].getblockhash(1000) diff --git a/test/functional/p2p_blocksonly.py b/test/functional/p2p_blocksonly.py index 646baa1550..e80422d1cf 100755 --- a/test/functional/p2p_blocksonly.py +++ b/test/functional/p2p_blocksonly.py @@ -59,7 +59,7 @@ class P2PBlocksOnly(BitcoinTestFramework): self.log.info('Check that txs from peers with relay-permission are not rejected and relayed to others') self.log.info("Restarting node 0 with relay permission and blocksonly") - self.restart_node(0, ["-persistmempool=0", "-whitelist=relay@127.0.0.1", "-blocksonly"]) + self.restart_node(0, ["-persistmempool=0", "-whitelist=relay@127.0.0.1", "-blocksonly", '-deprecatedrpc=whitelisted']) assert_equal(self.nodes[0].getrawmempool(), []) first_peer = self.nodes[0].add_p2p_connection(P2PInterface()) second_peer = self.nodes[0].add_p2p_connection(P2PInterface()) diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py index 2a320dbd99..9a9df73049 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -9,7 +9,7 @@ Version 2 compact blocks are post-segwit (wtxids) """ import random -from test_framework.blocktools import create_block, create_coinbase, add_witness_commitment +from test_framework.blocktools import create_block, NORMAL_GBT_REQUEST_PARAMS, add_witness_commitment from test_framework.messages import BlockTransactions, BlockTransactionsRequest, calculate_shortid, CBlock, CBlockHeader, CInv, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, FromHex, HeaderAndShortIDs, msg_no_witness_block, msg_no_witness_blocktxn, msg_cmpctblock, msg_getblocktxn, msg_getdata, msg_getheaders, msg_headers, msg_inv, msg_sendcmpct, msg_sendheaders, msg_tx, msg_block, msg_blocktxn, MSG_BLOCK, MSG_CMPCT_BLOCK, MSG_WITNESS_FLAG, NODE_NETWORK, P2PHeaderAndShortIDs, PrefilledTransaction, ser_uint256, ToHex from test_framework.p2p import p2p_lock, P2PInterface from test_framework.script import CScript, OP_TRUE, OP_DROP @@ -104,11 +104,7 @@ class CompactBlocksTest(BitcoinTestFramework): self.skip_if_no_wallet() def build_block_on_tip(self, node, segwit=False): - height = node.getblockcount() - tip = node.getbestblockhash() - mtp = node.getblockheader(tip)['mediantime'] - block = create_block(int(tip, 16), create_coinbase(height + 1), mtp + 1) - block.nVersion = 4 + block = create_block(tmpl=node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) if segwit: add_witness_commitment(block) block.solve() @@ -797,6 +793,9 @@ class CompactBlocksTest(BitcoinTestFramework): assert_highbandwidth_states(self.nodes[0], hb_to=True, hb_from=False) def run_test(self): + # Get the nodes out of IBD + self.nodes[0].generate(1) + # Setup the p2p connections self.segwit_node = self.nodes[0].add_p2p_connection(TestP2PConn(cmpct_version=2)) self.old_node = self.nodes[0].add_p2p_connection(TestP2PConn(cmpct_version=1), services=NODE_NETWORK) diff --git a/test/functional/p2p_disconnect_ban.py b/test/functional/p2p_disconnect_ban.py index b7c2a306eb..a7f51b5364 100755 --- a/test/functional/p2p_disconnect_ban.py +++ b/test/functional/p2p_disconnect_ban.py @@ -9,7 +9,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, ) class DisconnectBanTest(BitcoinTestFramework): @@ -19,8 +18,11 @@ class DisconnectBanTest(BitcoinTestFramework): def run_test(self): self.log.info("Connect nodes both way") - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 0) + # By default, the test framework sets up an addnode connection from + # node 1 --> node0. By connecting node0 --> node 1, we're left with + # the two nodes being connected both ways. + # Topology will look like: node0 <--> node1 + self.connect_nodes(0, 1) self.log.info("Test setban and listbanned RPCs") @@ -78,8 +80,8 @@ class DisconnectBanTest(BitcoinTestFramework): # Clear ban lists self.nodes[1].clearbanned() self.log.info("Connect nodes both way") - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 0) + self.connect_nodes(0, 1) + self.connect_nodes(1, 0) self.log.info("Test disconnectnode RPCs") @@ -98,7 +100,7 @@ class DisconnectBanTest(BitcoinTestFramework): assert not [node for node in self.nodes[0].getpeerinfo() if node['addr'] == address1] self.log.info("disconnectnode: successfully reconnect node") - connect_nodes(self.nodes[0], 1) # reconnect the node + self.connect_nodes(0, 1) # reconnect the node assert_equal(len(self.nodes[0].getpeerinfo()), 2) assert [node for node in self.nodes[0].getpeerinfo() if node['addr'] == address1] diff --git a/test/functional/p2p_feefilter.py b/test/functional/p2p_feefilter.py index ea066a984d..51b7623fe3 100755 --- a/test/functional/p2p_feefilter.py +++ b/test/functional/p2p_feefilter.py @@ -108,9 +108,10 @@ class FeeFilterTest(BitcoinTestFramework): # to 35 entries in an inv, which means that when this next transaction # is eligible for relay, the prior transactions from node1 are eligible # as well. - txids = [miniwallet.send_self_transfer(fee_rate=Decimal('0.00020000'), from_node=node0)['wtxid'] for _ in range(3)] + txids = [miniwallet.send_self_transfer(fee_rate=Decimal('0.00020000'), from_node=node0)['wtxid'] for _ in range(1)] conn.wait_for_invs_to_match(txids) conn.clear_invs() + self.sync_mempools() # must be sure node 1 has received all txs self.log.info("Remove fee filter and check txs are received again") conn.send_and_ping(msg_feefilter(0)) diff --git a/test/functional/p2p_fingerprint.py b/test/functional/p2p_fingerprint.py index aaf862e6c8..9614ab6872 100755 --- a/test/functional/p2p_fingerprint.py +++ b/test/functional/p2p_fingerprint.py @@ -18,6 +18,7 @@ from test_framework.p2p import ( msg_block, msg_getdata, msg_getheaders, + p2p_lock, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -57,18 +58,6 @@ class P2PFingerprintTest(BitcoinTestFramework): msg.hashstop = block_hash node.send_message(msg) - # Check whether last block received from node has a given hash - def last_block_equals(self, expected_hash, node): - block_msg = node.last_message.get("block") - return block_msg and block_msg.block.rehash() == expected_hash - - # Check whether last block header received from node has a given hash - def last_header_equals(self, expected_hash, node): - headers_msg = node.last_message.get("headers") - return (headers_msg and - headers_msg.headers and - headers_msg.headers[0].rehash() == expected_hash) - # Checks that stale blocks timestamped more than a month ago are not served # by the node while recent stale blocks and old active chain blocks are. # This does not currently test that stale blocks timestamped within the @@ -101,34 +90,31 @@ class P2PFingerprintTest(BitcoinTestFramework): # Check that getdata request for stale block succeeds self.send_block_request(stale_hash, node0) - test_function = lambda: self.last_block_equals(stale_hash, node0) - self.wait_until(test_function, timeout=3) + node0.wait_for_block(stale_hash, timeout=3) # Check that getheader request for stale block header succeeds self.send_header_request(stale_hash, node0) - test_function = lambda: self.last_header_equals(stale_hash, node0) - self.wait_until(test_function, timeout=3) + node0.wait_for_header(hex(stale_hash), timeout=3) # Longest chain is extended so stale is much older than chain tip self.nodes[0].setmocktime(0) - tip = self.nodes[0].generatetoaddress(1, self.nodes[0].get_deterministic_priv_key().address)[0] + block_hash = int(self.nodes[0].generatetoaddress(1, self.nodes[0].get_deterministic_priv_key().address)[-1], 16) assert_equal(self.nodes[0].getblockcount(), 14) - - # Send getdata & getheaders to refresh last received getheader message - block_hash = int(tip, 16) - self.send_block_request(block_hash, node0) - self.send_header_request(block_hash, node0) - node0.sync_with_ping() + node0.wait_for_block(block_hash, timeout=3) # Request for very old stale block should now fail + with p2p_lock: + node0.last_message.pop("block", None) self.send_block_request(stale_hash, node0) - time.sleep(3) - assert not self.last_block_equals(stale_hash, node0) + node0.sync_with_ping() + assert "block" not in node0.last_message # Request for very old stale block header should now fail + with p2p_lock: + node0.last_message.pop("headers", None) self.send_header_request(stale_hash, node0) - time.sleep(3) - assert not self.last_header_equals(stale_hash, node0) + node0.sync_with_ping() + assert "headers" not in node0.last_message # Verify we can fetch very old blocks and headers on the active chain block_hash = int(block_hashes[2], 16) @@ -137,12 +123,11 @@ class P2PFingerprintTest(BitcoinTestFramework): node0.sync_with_ping() self.send_block_request(block_hash, node0) - test_function = lambda: self.last_block_equals(block_hash, node0) - self.wait_until(test_function, timeout=3) + node0.wait_for_block(block_hash, timeout=3) self.send_header_request(block_hash, node0) - test_function = lambda: self.last_header_equals(block_hash, node0) - self.wait_until(test_function, timeout=3) + node0.wait_for_header(hex(block_hash), timeout=3) + if __name__ == '__main__': P2PFingerprintTest().main() diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index fe57057a83..db72a361d9 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -4,6 +4,9 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid network messages.""" +import struct +import time + from test_framework.messages import ( CBlockHeader, CInv, @@ -24,10 +27,12 @@ from test_framework.p2p import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + hex_str_to_bytes, ) VALID_DATA_LIMIT = MAX_PROTOCOL_MESSAGE_LENGTH - 5 # Account for the 5-byte length prefix + class msg_unrecognized: """Nonsensical message. Modeled after similar types in test_framework.messages.""" @@ -43,6 +48,11 @@ class msg_unrecognized: return "{}(data={})".format(self.msgtype, self.str_data) +class SenderOfAddrV2(P2PInterface): + def wait_for_sendaddrv2(self): + self.wait_until(lambda: 'sendaddrv2' in self.last_message) + + class InvalidMessagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -54,6 +64,10 @@ class InvalidMessagesTest(BitcoinTestFramework): self.test_checksum() self.test_size() self.test_msgtype() + self.test_addrv2_empty() + self.test_addrv2_no_addresses() + self.test_addrv2_too_long_address() + self.test_addrv2_unrecognized_network() self.test_oversized_inv_msg() self.test_oversized_getdata_msg() self.test_oversized_headers_msg() @@ -64,13 +78,13 @@ class InvalidMessagesTest(BitcoinTestFramework): conn = self.nodes[0].add_p2p_connection(P2PDataStore()) # Create valid message msg = conn.build_message(msg_ping(nonce=12345)) - cut_pos = 12 # Chosen at an arbitrary position within the header + cut_pos = 12 # Chosen at an arbitrary position within the header # Send message in two pieces - before = int(self.nodes[0].getnettotals()['totalbytesrecv']) + before = self.nodes[0].getnettotals()['totalbytesrecv'] conn.send_raw_message(msg[:cut_pos]) # Wait until node has processed the first half of the message - self.wait_until(lambda: int(self.nodes[0].getnettotals()['totalbytesrecv']) != before) - middle = int(self.nodes[0].getnettotals()['totalbytesrecv']) + self.wait_until(lambda: self.nodes[0].getnettotals()['totalbytesrecv'] != before) + middle = self.nodes[0].getnettotals()['totalbytesrecv'] # If this assert fails, we've hit an unlikely race # where the test framework sent a message in between the two halves assert_equal(middle, before + cut_pos) @@ -81,7 +95,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_magic_bytes(self): self.log.info("Test message with invalid magic bytes disconnects peer") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: INVALID MESSAGESTART badmsg']): + with self.nodes[0].assert_debug_log(['HEADER ERROR - MESSAGESTART (badmsg, 2 bytes), received ffffffff']): msg = conn.build_message(msg_unrecognized(str_data="d")) # modify magic bytes msg = b'\xff' * 4 + msg[4:] @@ -100,12 +114,14 @@ class InvalidMessagesTest(BitcoinTestFramework): msg = msg[:cut_len] + b'\xff' * 4 + msg[cut_len + 4:] conn.send_raw_message(msg) conn.sync_with_ping(timeout=1) + # Check that traffic is accounted for (24 bytes header + 2 bytes payload) + assert_equal(self.nodes[0].getpeerinfo()[0]['bytesrecv_per_msg']['*other*'], 26) self.nodes[0].disconnect_p2ps() def test_size(self): self.log.info("Test message with oversized payload disconnects peer") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['']): + with self.nodes[0].assert_debug_log(['HEADER ERROR - SIZE (badmsg, 4000001 bytes)']): msg = msg_unrecognized(str_data="d" * (VALID_DATA_LIMIT + 1)) msg = conn.build_message(msg) conn.send_raw_message(msg) @@ -115,16 +131,95 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_msgtype(self): self.log.info("Test message with invalid message type logs an error") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: ERRORS IN HEADER']): + with self.nodes[0].assert_debug_log(['HEADER ERROR - COMMAND']): msg = msg_unrecognized(str_data="d") - msg.msgtype = b'\xff' * 12 msg = conn.build_message(msg) # Modify msgtype msg = msg[:7] + b'\x00' + msg[7 + 1:] conn.send_raw_message(msg) conn.sync_with_ping(timeout=1) + # Check that traffic is accounted for (24 bytes header + 2 bytes payload) + assert_equal(self.nodes[0].getpeerinfo()[0]['bytesrecv_per_msg']['*other*'], 26) self.nodes[0].disconnect_p2ps() + def test_addrv2(self, label, required_log_messages, raw_addrv2): + node = self.nodes[0] + conn = node.add_p2p_connection(SenderOfAddrV2()) + + # Make sure bitcoind signals support for ADDRv2, otherwise this test + # will bombard an old node with messages it does not recognize which + # will produce unexpected results. + conn.wait_for_sendaddrv2() + + self.log.info('Test addrv2: ' + label) + + msg = msg_unrecognized(str_data=b'') + msg.msgtype = b'addrv2' + with node.assert_debug_log(required_log_messages): + # override serialize() which would include the length of the data + msg.serialize = lambda: raw_addrv2 + conn.send_raw_message(conn.build_message(msg)) + conn.sync_with_ping() + + node.disconnect_p2ps() + + def test_addrv2_empty(self): + self.test_addrv2('empty', + [ + 'received: addrv2 (0 bytes)', + 'ProcessMessages(addrv2, 0 bytes): Exception', + 'end of data', + ], + b'') + + def test_addrv2_no_addresses(self): + self.test_addrv2('no addresses', + [ + 'received: addrv2 (1 bytes)', + ], + hex_str_to_bytes('00')) + + def test_addrv2_too_long_address(self): + self.test_addrv2('too long address', + [ + 'received: addrv2 (525 bytes)', + 'ProcessMessages(addrv2, 525 bytes): Exception', + 'Address too long: 513 > 512', + ], + hex_str_to_bytes( + '01' + # number of entries + '61bc6649' + # time, Fri Jan 9 02:54:25 UTC 2009 + '00' + # service flags, COMPACTSIZE(NODE_NONE) + '01' + # network type (IPv4) + 'fd0102' + # address length (COMPACTSIZE(513)) + 'ab' * 513 + # address + '208d')) # port + + def test_addrv2_unrecognized_network(self): + now_hex = struct.pack('<I', int(time.time())).hex() + self.test_addrv2('unrecognized network', + [ + 'received: addrv2 (25 bytes)', + 'IP 9.9.9.9 mapped', + 'Added 1 addresses', + ], + hex_str_to_bytes( + '02' + # number of entries + # this should be ignored without impeding acceptance of subsequent ones + now_hex + # time + '01' + # service flags, COMPACTSIZE(NODE_NETWORK) + '99' + # network type (unrecognized) + '02' + # address length (COMPACTSIZE(2)) + 'ab' * 2 + # address + '208d' + # port + # this should be added: + now_hex + # time + '01' + # service flags, COMPACTSIZE(NODE_NETWORK) + '01' + # network type (IPv4) + '04' + # address length (COMPACTSIZE(4)) + '09' * 4 + # address + '208d')) # port + def test_oversized_msg(self, msg, size): msg_type = msg.msgtype.decode('ascii') self.log.info("Test {} message of size {} is logged as misbehaving".format(msg_type, size)) diff --git a/test/functional/p2p_leak_tx.py b/test/functional/p2p_leak_tx.py index 9e761db03f..a45f792e81 100755 --- a/test/functional/p2p_leak_tx.py +++ b/test/functional/p2p_leak_tx.py @@ -5,11 +5,12 @@ """Test that we don't leak txs to inbound peers that we haven't yet announced to""" from test_framework.messages import msg_getdata, CInv, MSG_TX -from test_framework.p2p import P2PDataStore +from test_framework.p2p import p2p_lock, P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, ) +from test_framework.wallet import MiniWallet class P2PNode(P2PDataStore): @@ -21,12 +22,12 @@ class P2PLeakTxTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): gen_node = self.nodes[0] # The block and tx generating node - gen_node.generate(1) + miniwallet = MiniWallet(gen_node) + # Add enough mature utxos to the wallet, so that all txs spend confirmed coins + miniwallet.generate(1) + gen_node.generate(100) inbound_peer = self.nodes[0].add_p2p_connection(P2PNode()) # An "attacking" inbound peer @@ -34,18 +35,20 @@ class P2PLeakTxTest(BitcoinTestFramework): self.log.info("Running test up to {} times.".format(MAX_REPEATS)) for i in range(MAX_REPEATS): self.log.info('Run repeat {}'.format(i + 1)) - txid = gen_node.sendtoaddress(gen_node.getnewaddress(), 0.01) + txid = miniwallet.send_self_transfer(from_node=gen_node)['wtxid'] want_tx = msg_getdata() want_tx.inv.append(CInv(t=MSG_TX, h=int(txid, 16))) - inbound_peer.last_message.pop('notfound', None) + with p2p_lock: + inbound_peer.last_message.pop('notfound', None) inbound_peer.send_and_ping(want_tx) if inbound_peer.last_message.get('notfound'): self.log.debug('tx {} was not yet announced to us.'.format(txid)) self.log.debug("node has responded with a notfound message. End test.") assert_equal(inbound_peer.last_message['notfound'].vec[0].hash, int(txid, 16)) - inbound_peer.last_message.pop('notfound') + with p2p_lock: + inbound_peer.last_message.pop('notfound') break else: self.log.debug('tx {} was already announced to us. Try test again.'.format(txid)) diff --git a/test/functional/p2p_node_network_limited.py b/test/functional/p2p_node_network_limited.py index 2c9cbea5e4..b1a7ef6877 100755 --- a/test/functional/p2p_node_network_limited.py +++ b/test/functional/p2p_node_network_limited.py @@ -13,8 +13,6 @@ from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - disconnect_nodes, - connect_nodes, ) @@ -40,9 +38,9 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): self.extra_args = [['-prune=550', '-addrmantest'], [], []] def disconnect_all(self): - disconnect_nodes(self.nodes[0], 1) - disconnect_nodes(self.nodes[0], 2) - disconnect_nodes(self.nodes[1], 2) + self.disconnect_nodes(0, 1) + self.disconnect_nodes(0, 2) + self.disconnect_nodes(1, 2) def setup_network(self): self.add_nodes(self.num_nodes, self.extra_args) @@ -60,7 +58,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): assert_equal(int(self.nodes[0].getnetworkinfo()['localservices'], 16), expected_services) self.log.info("Mine enough blocks to reach the NODE_NETWORK_LIMITED range.") - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) blocks = self.nodes[1].generatetoaddress(292, self.nodes[1].get_deterministic_priv_key().address) self.sync_blocks([self.nodes[0], self.nodes[1]]) @@ -85,7 +83,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): # connect unsynced node 2 with pruned NODE_NETWORK_LIMITED peer # because node 2 is in IBD and node 0 is a NODE_NETWORK_LIMITED peer, sync must not be possible - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) try: self.sync_blocks([self.nodes[0], self.nodes[2]], timeout=5) except: @@ -94,7 +92,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): assert_equal(self.nodes[2].getblockheader(self.nodes[2].getbestblockhash())['height'], 0) # now connect also to node 1 (non pruned) - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) # sync must be possible self.sync_blocks() @@ -106,7 +104,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) # connect node1 (non pruned) with node0 (pruned) and check if the can sync - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) # sync must be possible, node 1 is no longer in IBD and should therefore connect to node 0 (NODE_NETWORK_LIMITED) self.sync_blocks([self.nodes[0], self.nodes[1]]) diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 3ec36edb41..ed82e6a2e2 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -22,7 +22,6 @@ from test_framework.test_node import ErrorMatch from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, p2p_port, ) @@ -43,6 +42,13 @@ class P2PPermissionsTests(BitcoinTestFramework): True) self.checkpermission( + # check without deprecatedrpc=whitelisted + ["-whitelist=127.0.0.1"], + # Make sure the default values in the command line documentation match the ones here + ["relay", "noban", "mempool", "download"], + None) + + self.checkpermission( # no permission (even with forcerelay) ["-whitelist=@127.0.0.1", "-whitelistforcerelay=1"], [], @@ -81,6 +87,12 @@ class P2PPermissionsTests(BitcoinTestFramework): False) self.checkpermission( + # check without deprecatedrpc=whitelisted + ["-whitelist=noban,mempool@127.0.0.1", "-whitelistrelay"], + ["noban", "mempool", "download"], + None) + + self.checkpermission( # legacy whitelistforcerelay should be ignored ["-whitelist=noban,mempool@127.0.0.1", "-whitelistforcerelay"], ["noban", "mempool", "download"], @@ -133,7 +145,7 @@ class P2PPermissionsTests(BitcoinTestFramework): p2p_rebroadcast_wallet.send_txs_and_test([tx], self.nodes[1]) self.log.debug("Check that node[1] will send the tx to node[0] even though it is already in the mempool") - connect_nodes(self.nodes[1], 0) + self.connect_nodes(1, 0) with self.nodes[1].assert_debug_log(["Force relaying tx {} from peer=0".format(txid)]): p2p_rebroadcast_wallet.send_txs_and_test([tx], self.nodes[1]) self.wait_until(lambda: txid in self.nodes[0].getrawmempool()) @@ -141,18 +153,32 @@ class P2PPermissionsTests(BitcoinTestFramework): self.log.debug("Check that node[1] will not send an invalid tx to node[0]") tx.vout[0].nValue += 1 txid = tx.rehash() + # Send the transaction twice. The first time, it'll be rejected by ATMP because it conflicts + # with a mempool transaction. The second time, it'll be in the recentRejects filter. + p2p_rebroadcast_wallet.send_txs_and_test( + [tx], + self.nodes[1], + success=False, + reject_reason='{} from peer=0 was not accepted: txn-mempool-conflict'.format(txid) + ) + p2p_rebroadcast_wallet.send_txs_and_test( [tx], self.nodes[1], success=False, - reject_reason='Not relaying non-mempool transaction {} from forcerelay peer=0'.format(txid), + reject_reason='Not relaying non-mempool transaction {} from forcerelay peer=0'.format(txid) ) def checkpermission(self, args, expectedPermissions, whitelisted): + if whitelisted is not None: + args = [*args, '-deprecatedrpc=whitelisted'] self.restart_node(1, args) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] - assert_equal(peerinfo['whitelisted'], whitelisted) + if whitelisted is None: + assert 'whitelisted' not in peerinfo + else: + assert_equal(peerinfo['whitelisted'], whitelisted) assert_equal(len(expectedPermissions), len(peerinfo['permissions'])) for p in expectedPermissions: if not p in peerinfo['permissions']: diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index d79ed449e5..e99ecd8026 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -55,6 +55,7 @@ from test_framework.script import ( MAX_SCRIPT_ELEMENT_SIZE, OP_0, OP_1, + OP_2, OP_16, OP_2DROP, OP_CHECKMULTISIG, @@ -80,8 +81,6 @@ from test_framework.script import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, softfork_active, hex_str_to_bytes, assert_raises_rpc_error, @@ -232,8 +231,8 @@ class SegWitTest(BitcoinTestFramework): def setup_network(self): self.setup_nodes() - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) self.sync_all() # Helper functions @@ -497,7 +496,7 @@ class SegWitTest(BitcoinTestFramework): # node2 doesn't need to be connected for this test. # (If it's connected, node0 may propagate an invalid block to it over # compact blocks and the nodes would have inconsistent tips.) - disconnect_nodes(self.nodes[0], 2) + self.disconnect_nodes(0, 2) # Create two outputs, a p2wsh and p2sh-p2wsh witness_program = CScript([OP_TRUE]) @@ -559,7 +558,7 @@ class SegWitTest(BitcoinTestFramework): # TODO: support multiple acceptable reject reasons. test_witness_block(self.nodes[0], self.test_node, block, accepted=False, with_witness=False) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) self.utxo.pop(0) self.utxo.append(UTXO(txid, 2, value)) @@ -1400,7 +1399,11 @@ class SegWitTest(BitcoinTestFramework): assert_equal(len(self.nodes[1].getrawmempool()), 0) for version in list(range(OP_1, OP_16 + 1)) + [OP_0]: # First try to spend to a future version segwit script_pubkey. - script_pubkey = CScript([CScriptOp(version), witness_hash]) + if version == OP_1: + # Don't use 32-byte v1 witness (used by Taproot; see BIP 341) + script_pubkey = CScript([CScriptOp(version), witness_hash + b'\x00']) + else: + script_pubkey = CScript([CScriptOp(version), witness_hash]) tx.vin = [CTxIn(COutPoint(self.utxo[0].sha256, self.utxo[0].n), b"")] tx.vout = [CTxOut(self.utxo[0].nValue - 1000, script_pubkey)] tx.rehash() @@ -1413,9 +1416,9 @@ class SegWitTest(BitcoinTestFramework): self.sync_blocks() assert len(self.nodes[0].getrawmempool()) == 0 - # Finally, verify that version 0 -> version 1 transactions + # Finally, verify that version 0 -> version 2 transactions # are standard - script_pubkey = CScript([CScriptOp(OP_1), witness_hash]) + script_pubkey = CScript([CScriptOp(OP_2), witness_hash]) tx2 = CTransaction() tx2.vin = [CTxIn(COutPoint(tx.sha256, 0), b"")] tx2.vout = [CTxOut(tx.vout[0].nValue - 1000, script_pubkey)] @@ -1940,7 +1943,7 @@ class SegWitTest(BitcoinTestFramework): """Test the behavior of starting up a segwit-aware node after the softfork has activated.""" self.restart_node(2, extra_args=["-segwitheight={}".format(SEGWIT_HEIGHT)]) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) # We reconnect more than 100 blocks, give it plenty of time self.sync_blocks(timeout=240) @@ -2094,14 +2097,14 @@ class SegWitTest(BitcoinTestFramework): raw = self.nodes[0].createrawtransaction([{"txid": unspent['txid'], "vout": unspent['vout']}], {self.nodes[0].getnewaddress(): 1}) tx = FromHex(CTransaction(), raw) - assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, serialize_with_bogus_witness(tx).hex()) + assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Superfluous witness record']): self.test_node.send_and_ping(msg_bogus_tx(tx)) raw = self.nodes[0].signrawtransactionwithwallet(raw) assert raw['complete'] raw = raw['hex'] tx = FromHex(CTransaction(), raw) - assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, serialize_with_bogus_witness(tx).hex()) + assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Unknown transaction optional data']): self.test_node.send_and_ping(msg_bogus_tx(tx)) diff --git a/test/functional/p2p_tx_download.py b/test/functional/p2p_tx_download.py index 653c7ae43f..16d9302db8 100755 --- a/test/functional/p2p_tx_download.py +++ b/test/functional/p2p_tx_download.py @@ -42,15 +42,15 @@ class TestP2PConn(P2PInterface): # Constants from net_processing GETDATA_TX_INTERVAL = 60 # seconds -MAX_GETDATA_RANDOM_DELAY = 2 # seconds INBOUND_PEER_TX_DELAY = 2 # seconds TXID_RELAY_DELAY = 2 # seconds +OVERLOADED_PEER_DELAY = 2 # seconds MAX_GETDATA_IN_FLIGHT = 100 -TX_EXPIRY_INTERVAL = GETDATA_TX_INTERVAL * 10 +MAX_PEER_TX_ANNOUNCEMENTS = 5000 # Python test constants NUM_INBOUND = 10 -MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY + TXID_RELAY_DELAY +MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + INBOUND_PEER_TX_DELAY + TXID_RELAY_DELAY class TxDownloadTest(BitcoinTestFramework): @@ -121,14 +121,12 @@ class TxDownloadTest(BitcoinTestFramework): # * the first time it is re-requested from the outbound peer, plus # * 2 seconds to avoid races assert self.nodes[1].getpeerinfo()[0]['inbound'] == False - timeout = 2 + (MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY) + ( - GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY) + timeout = 2 + INBOUND_PEER_TX_DELAY + GETDATA_TX_INTERVAL self.log.info("Tx should be received at node 1 after {} seconds".format(timeout)) self.sync_mempools(timeout=timeout) def test_in_flight_max(self): - self.log.info("Test that we don't request more than {} transactions from any peer, every {} minutes".format( - MAX_GETDATA_IN_FLIGHT, TX_EXPIRY_INTERVAL / 60)) + self.log.info("Test that we don't load peers with more than {} transaction requests immediately".format(MAX_GETDATA_IN_FLIGHT)) txids = [i for i in range(MAX_GETDATA_IN_FLIGHT + 2)] p = self.nodes[0].p2ps[0] @@ -136,45 +134,130 @@ class TxDownloadTest(BitcoinTestFramework): with p2p_lock: p.tx_getdata_count = 0 - p.send_message(msg_inv([CInv(t=MSG_WTX, h=i) for i in txids])) + mock_time = int(time.time() + 1) + self.nodes[0].setmocktime(mock_time) + for i in range(MAX_GETDATA_IN_FLIGHT): + p.send_message(msg_inv([CInv(t=MSG_WTX, h=txids[i])])) + p.sync_with_ping() + mock_time += INBOUND_PEER_TX_DELAY + self.nodes[0].setmocktime(mock_time) p.wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT) + for i in range(MAX_GETDATA_IN_FLIGHT, len(txids)): + p.send_message(msg_inv([CInv(t=MSG_WTX, h=txids[i])])) + p.sync_with_ping() + self.log.info("No more than {} requests should be seen within {} seconds after announcement".format(MAX_GETDATA_IN_FLIGHT, INBOUND_PEER_TX_DELAY + OVERLOADED_PEER_DELAY - 1)) + self.nodes[0].setmocktime(mock_time + INBOUND_PEER_TX_DELAY + OVERLOADED_PEER_DELAY - 1) + p.sync_with_ping() with p2p_lock: assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT) - - self.log.info("Now check that if we send a NOTFOUND for a transaction, we'll get one more request") - p.send_message(msg_notfound(vec=[CInv(t=MSG_WTX, h=txids[0])])) - p.wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT + 1, timeout=10) + self.log.info("If we wait {} seconds after announcement, we should eventually get more requests".format(INBOUND_PEER_TX_DELAY + OVERLOADED_PEER_DELAY)) + self.nodes[0].setmocktime(mock_time + INBOUND_PEER_TX_DELAY + OVERLOADED_PEER_DELAY) + p.wait_until(lambda: p.tx_getdata_count == len(txids)) + + def test_expiry_fallback(self): + self.log.info('Check that expiry will select another peer for download') + WTXID = 0xffaa + peer1 = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer2 = self.nodes[0].add_p2p_connection(TestP2PConn()) + for p in [peer1, peer2]: + p.send_message(msg_inv([CInv(t=MSG_WTX, h=WTXID)])) + # One of the peers is asked for the tx + peer2.wait_until(lambda: sum(p.tx_getdata_count for p in [peer1, peer2]) == 1) with p2p_lock: - assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT + 1) - - WAIT_TIME = TX_EXPIRY_INTERVAL // 2 + TX_EXPIRY_INTERVAL - self.log.info("if we wait about {} minutes, we should eventually get more requests".format(WAIT_TIME / 60)) - self.nodes[0].setmocktime(int(time.time() + WAIT_TIME)) - p.wait_until(lambda: p.tx_getdata_count == MAX_GETDATA_IN_FLIGHT + 2) - self.nodes[0].setmocktime(0) + peer_expiry, peer_fallback = (peer1, peer2) if peer1.tx_getdata_count == 1 else (peer2, peer1) + assert_equal(peer_fallback.tx_getdata_count, 0) + self.nodes[0].setmocktime(int(time.time()) + GETDATA_TX_INTERVAL + 1) # Wait for request to peer_expiry to expire + peer_fallback.wait_until(lambda: peer_fallback.tx_getdata_count >= 1, timeout=1) + with p2p_lock: + assert_equal(peer_fallback.tx_getdata_count, 1) + self.restart_node(0) # reset mocktime + + def test_disconnect_fallback(self): + self.log.info('Check that disconnect will select another peer for download') + WTXID = 0xffbb + peer1 = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer2 = self.nodes[0].add_p2p_connection(TestP2PConn()) + for p in [peer1, peer2]: + p.send_message(msg_inv([CInv(t=MSG_WTX, h=WTXID)])) + # One of the peers is asked for the tx + peer2.wait_until(lambda: sum(p.tx_getdata_count for p in [peer1, peer2]) == 1) + with p2p_lock: + peer_disconnect, peer_fallback = (peer1, peer2) if peer1.tx_getdata_count == 1 else (peer2, peer1) + assert_equal(peer_fallback.tx_getdata_count, 0) + peer_disconnect.peer_disconnect() + peer_disconnect.wait_for_disconnect() + peer_fallback.wait_until(lambda: peer_fallback.tx_getdata_count >= 1, timeout=1) + with p2p_lock: + assert_equal(peer_fallback.tx_getdata_count, 1) + + def test_notfound_fallback(self): + self.log.info('Check that notfounds will select another peer for download immediately') + WTXID = 0xffdd + peer1 = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer2 = self.nodes[0].add_p2p_connection(TestP2PConn()) + for p in [peer1, peer2]: + p.send_message(msg_inv([CInv(t=MSG_WTX, h=WTXID)])) + # One of the peers is asked for the tx + peer2.wait_until(lambda: sum(p.tx_getdata_count for p in [peer1, peer2]) == 1) + with p2p_lock: + peer_notfound, peer_fallback = (peer1, peer2) if peer1.tx_getdata_count == 1 else (peer2, peer1) + assert_equal(peer_fallback.tx_getdata_count, 0) + peer_notfound.send_and_ping(msg_notfound(vec=[CInv(MSG_WTX, WTXID)])) # Send notfound, so that fallback peer is selected + peer_fallback.wait_until(lambda: peer_fallback.tx_getdata_count >= 1, timeout=1) + with p2p_lock: + assert_equal(peer_fallback.tx_getdata_count, 1) + + def test_preferred_inv(self): + self.log.info('Check that invs from preferred peers are downloaded immediately') + self.restart_node(0, extra_args=['-whitelist=noban@127.0.0.1']) + peer = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)])) + peer.wait_until(lambda: peer.tx_getdata_count >= 1, timeout=1) + with p2p_lock: + assert_equal(peer.tx_getdata_count, 1) + + def test_large_inv_batch(self): + self.log.info('Test how large inv batches are handled with relay permission') + self.restart_node(0, extra_args=['-whitelist=relay@127.0.0.1']) + peer = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer.send_message(msg_inv([CInv(t=MSG_WTX, h=wtxid) for wtxid in range(MAX_PEER_TX_ANNOUNCEMENTS + 1)])) + peer.wait_until(lambda: peer.tx_getdata_count == MAX_PEER_TX_ANNOUNCEMENTS + 1) + + self.log.info('Test how large inv batches are handled without relay permission') + self.restart_node(0) + peer = self.nodes[0].add_p2p_connection(TestP2PConn()) + peer.send_message(msg_inv([CInv(t=MSG_WTX, h=wtxid) for wtxid in range(MAX_PEER_TX_ANNOUNCEMENTS + 1)])) + peer.wait_until(lambda: peer.tx_getdata_count == MAX_PEER_TX_ANNOUNCEMENTS) + peer.sync_with_ping() + with p2p_lock: + assert_equal(peer.tx_getdata_count, MAX_PEER_TX_ANNOUNCEMENTS) def test_spurious_notfound(self): self.log.info('Check that spurious notfound is ignored') self.nodes[0].p2ps[0].send_message(msg_notfound(vec=[CInv(MSG_TX, 1)])) def run_test(self): - # Setup the p2p connections - self.peers = [] - for node in self.nodes: - for _ in range(NUM_INBOUND): - self.peers.append(node.add_p2p_connection(TestP2PConn())) - - self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND)) - + # Run tests without mocktime that only need one peer-connection first, to avoid restarting the nodes + self.test_expiry_fallback() + self.test_disconnect_fallback() + self.test_notfound_fallback() + self.test_preferred_inv() + self.test_large_inv_batch() self.test_spurious_notfound() - # Test the in-flight max first, because we want no transactions in - # flight ahead of this test. - self.test_in_flight_max() - - self.test_inv_block() - - self.test_tx_requests() + # Run each test against new bitcoind instances, as setting mocktimes has long-term effects on when + # the next trickle relay event happens. + for test in [self.test_in_flight_max, self.test_inv_block, self.test_tx_requests]: + self.stop_nodes() + self.start_nodes() + self.connect_nodes(1, 0) + # Setup the p2p connections + self.peers = [] + for node in self.nodes: + for _ in range(NUM_INBOUND): + self.peers.append(node.add_p2p_connection(TestP2PConn())) + self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND)) + test() if __name__ == '__main__': diff --git a/test/functional/p2p_unrequested_blocks.py b/test/functional/p2p_unrequested_blocks.py index 36b434bce3..e7a05d8547 100755 --- a/test/functional/p2p_unrequested_blocks.py +++ b/test/functional/p2p_unrequested_blocks.py @@ -60,7 +60,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, ) @@ -284,7 +283,7 @@ class AcceptBlockTest(BitcoinTestFramework): test_node.wait_for_disconnect() # 9. Connect node1 to node0 and ensure it is able to sync - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks([self.nodes[0], self.nodes[1]]) self.log.info("Successfully synced nodes 1 and 0") diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 35cea85c07..f965677408 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -146,7 +146,19 @@ class BlockchainTest(BitcoinTestFramework): 'possible': True, }, }, - 'active': False} + 'active': False + }, + 'taproot': { + 'type': 'bip9', + 'bip9': { + 'status': 'active', + 'start_time': -1, + 'timeout': 9223372036854775807, + 'since': 0 + }, + 'height': 0, + 'active': True + } }) def _test_getchaintxstats(self): diff --git a/test/functional/rpc_deprecated.py b/test/functional/rpc_deprecated.py index b71854d234..adcd8a7d4c 100755 --- a/test/functional/rpc_deprecated.py +++ b/test/functional/rpc_deprecated.py @@ -29,7 +29,7 @@ class DeprecatedRpcTest(BitcoinTestFramework): self.nodes[0].generate(101) self.nodes[0].createwallet(wallet_name='nopriv', disable_private_keys=True) noprivs0 = self.nodes[0].get_wallet_rpc('nopriv') - w0 = self.nodes[0].get_wallet_rpc('') + w0 = self.nodes[0].get_wallet_rpc(self.default_wallet_name) self.nodes[1].createwallet(wallet_name='nopriv', disable_private_keys=True) noprivs1 = self.nodes[1].get_wallet_rpc('nopriv') diff --git a/test/functional/rpc_estimatefee.py b/test/functional/rpc_estimatefee.py index 1fff9e1512..81862ac69e 100755 --- a/test/functional/rpc_estimatefee.py +++ b/test/functional/rpc_estimatefee.py @@ -28,7 +28,7 @@ class EstimateFeeTest(BitcoinTestFramework): # wrong type for estimatesmartfee(estimate_mode) assert_raises_rpc_error(-3, "Expected type string, got number", self.nodes[0].estimatesmartfee, 1, 1) - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", self.nodes[0].estimatesmartfee, 1, 'foo') + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', self.nodes[0].estimatesmartfee, 1, 'foo') # wrong type for estimaterawfee(threshold) assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimaterawfee, 1, 'foo') @@ -41,6 +41,8 @@ class EstimateFeeTest(BitcoinTestFramework): self.nodes[0].estimatesmartfee(1) # self.nodes[0].estimatesmartfee(1, None) self.nodes[0].estimatesmartfee(1, 'ECONOMICAL') + self.nodes[0].estimatesmartfee(1, 'unset') + self.nodes[0].estimatesmartfee(1, 'conservative') self.nodes[0].estimaterawfee(1) self.nodes[0].estimaterawfee(1, None) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 6dcbec2714..8ee0ecab0a 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -5,14 +5,15 @@ """Test the fundrawtransaction RPC.""" from decimal import Decimal +from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( + assert_approx, assert_equal, assert_fee_amount, assert_greater_than, assert_greater_than_or_equal, assert_raises_rpc_error, - connect_nodes, count_bytes, find_vout_for_address, ) @@ -38,10 +39,10 @@ class RawTransactionsTest(BitcoinTestFramework): def setup_network(self): self.setup_nodes() - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) - connect_nodes(self.nodes[0], 3) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) + self.connect_nodes(0, 3) def run_test(self): self.log.info("Connect nodes, set fees, generate blocks, and sync") @@ -101,17 +102,19 @@ class RawTransactionsTest(BitcoinTestFramework): rawmatch = self.nodes[2].fundrawtransaction(rawmatch, {"changePosition":1, "subtractFeeFromOutputs":[0]}) assert_equal(rawmatch["changepos"], -1) + self.nodes[3].createwallet(wallet_name="wwatch", disable_private_keys=True) + wwatch = self.nodes[3].get_wallet_rpc('wwatch') watchonly_address = self.nodes[0].getnewaddress() watchonly_pubkey = self.nodes[0].getaddressinfo(watchonly_address)["pubkey"] self.watchonly_amount = Decimal(200) - self.nodes[3].importpubkey(watchonly_pubkey, "", True) + wwatch.importpubkey(watchonly_pubkey, "", True) self.watchonly_txid = self.nodes[0].sendtoaddress(watchonly_address, self.watchonly_amount) # Lock UTXO so nodes[0] doesn't accidentally spend it self.watchonly_vout = find_vout_for_address(self.nodes[0], self.watchonly_txid, watchonly_address) self.nodes[0].lockunspent(False, [{"txid": self.watchonly_txid, "vout": self.watchonly_vout}]) - self.nodes[0].sendtoaddress(self.nodes[3].getnewaddress(), self.watchonly_amount / 10) + self.nodes[0].sendtoaddress(self.nodes[3].get_wallet_rpc(self.default_wallet_name).getnewaddress(), self.watchonly_amount / 10) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.5) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.0) @@ -120,6 +123,8 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() + wwatch.unloadwallet() + def test_simple(self): self.log.info("Test fundrawtxn") inputs = [ ] @@ -407,7 +412,7 @@ class RawTransactionsTest(BitcoinTestFramework): addr1Obj = self.nodes[1].getaddressinfo(addr1) addr2Obj = self.nodes[1].getaddressinfo(addr2) - mSigObj = self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObj = self.nodes[3].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] inputs = [] outputs = {mSigObj:1.1} @@ -439,7 +444,7 @@ class RawTransactionsTest(BitcoinTestFramework): addr4Obj = self.nodes[1].getaddressinfo(addr4) addr5Obj = self.nodes[1].getaddressinfo(addr5) - mSigObj = self.nodes[1].addmultisigaddress( + mSigObj = self.nodes[1].createmultisig( 4, [ addr1Obj['pubkey'], @@ -465,7 +470,7 @@ class RawTransactionsTest(BitcoinTestFramework): def test_spend_2of2(self): """Spend a 2-of-2 multisig transaction over fundraw.""" - self.log.info("Test fundrawtxn spending 2-of-2 multisig") + self.log.info("Test fundpsbt spending 2-of-2 multisig") # Create 2-of-2 addr. addr1 = self.nodes[2].getnewaddress() @@ -474,13 +479,18 @@ class RawTransactionsTest(BitcoinTestFramework): addr1Obj = self.nodes[2].getaddressinfo(addr1) addr2Obj = self.nodes[2].getaddressinfo(addr2) - mSigObj = self.nodes[2].addmultisigaddress( + self.nodes[2].createwallet(wallet_name='wmulti', disable_private_keys=True) + wmulti = self.nodes[2].get_wallet_rpc('wmulti') + w2 = self.nodes[2].get_wallet_rpc(self.default_wallet_name) + mSigObj = wmulti.addmultisigaddress( 2, [ addr1Obj['pubkey'], addr2Obj['pubkey'], ] )['address'] + if not self.options.descriptors: + wmulti.importaddress(mSigObj) # Send 1.2 BTC to msig addr. self.nodes[0].sendtoaddress(mSigObj, 1.2) @@ -490,22 +500,39 @@ class RawTransactionsTest(BitcoinTestFramework): oldBalance = self.nodes[1].getbalance() inputs = [] outputs = {self.nodes[1].getnewaddress():1.1} - rawtx = self.nodes[2].createrawtransaction(inputs, outputs) - fundedTx = self.nodes[2].fundrawtransaction(rawtx) + funded_psbt = wmulti.walletcreatefundedpsbt(inputs=inputs, outputs=outputs, options={'changeAddress': w2.getrawchangeaddress()})['psbt'] - signedTx = self.nodes[2].signrawtransactionwithwallet(fundedTx['hex']) - self.nodes[2].sendrawtransaction(signedTx['hex']) + signed_psbt = w2.walletprocesspsbt(funded_psbt) + final_psbt = w2.finalizepsbt(signed_psbt['psbt']) + self.nodes[2].sendrawtransaction(final_psbt['hex']) self.nodes[2].generate(1) self.sync_all() # Make sure funds are received at node1. assert_equal(oldBalance+Decimal('1.10000000'), self.nodes[1].getbalance()) + wmulti.unloadwallet() + def test_locked_wallet(self): - self.log.info("Test fundrawtxn with locked wallet") + self.log.info("Test fundrawtxn with locked wallet and hardened derivation") self.nodes[1].encryptwallet("test") + if self.options.descriptors: + self.nodes[1].walletpassphrase('test', 10) + self.nodes[1].importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + self.nodes[1].walletlock() + # Drain the keypool. self.nodes[1].getnewaddress() self.nodes[1].getrawchangeaddress() @@ -622,7 +649,25 @@ class RawTransactionsTest(BitcoinTestFramework): outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount / 2} rawtx = self.nodes[3].createrawtransaction(inputs, outputs) - result = self.nodes[3].fundrawtransaction(rawtx, {'includeWatching': True }) + self.nodes[3].loadwallet('wwatch') + wwatch = self.nodes[3].get_wallet_rpc('wwatch') + # Setup change addresses for the watchonly wallet + desc_import = [{ + "desc": descsum_create("wpkh(tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H/1/*)"), + "timestamp": "now", + "internal": True, + "active": True, + "keypool": True, + "range": [0, 100], + "watchonly": True, + }] + if self.options.descriptors: + wwatch.importdescriptors(desc_import) + else: + wwatch.importmulti(desc_import) + + # Backward compatibility test (2nd params is includeWatching) + result = wwatch.fundrawtransaction(rawtx, True) res_dec = self.nodes[0].decoderawtransaction(result["hex"]) assert_equal(len(res_dec["vin"]), 1) assert_equal(res_dec["vin"][0]["txid"], self.watchonly_txid) @@ -630,6 +675,8 @@ class RawTransactionsTest(BitcoinTestFramework): assert "fee" in result.keys() assert_greater_than(result["changepos"], -1) + wwatch.unloadwallet() + def test_all_watched_funds(self): self.log.info("Test fundrawtxn using entirety of watched funds") @@ -637,17 +684,19 @@ class RawTransactionsTest(BitcoinTestFramework): outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount} rawtx = self.nodes[3].createrawtransaction(inputs, outputs) - # Backward compatibility test (2nd param is includeWatching). - result = self.nodes[3].fundrawtransaction(rawtx, True) + self.nodes[3].loadwallet('wwatch') + wwatch = self.nodes[3].get_wallet_rpc('wwatch') + w3 = self.nodes[3].get_wallet_rpc(self.default_wallet_name) + result = wwatch.fundrawtransaction(rawtx, {'includeWatching': True, 'changeAddress': w3.getrawchangeaddress(), 'subtractFeeFromOutputs': [0]}) res_dec = self.nodes[0].decoderawtransaction(result["hex"]) - assert_equal(len(res_dec["vin"]), 2) - assert res_dec["vin"][0]["txid"] == self.watchonly_txid or res_dec["vin"][1]["txid"] == self.watchonly_txid + assert_equal(len(res_dec["vin"]), 1) + assert res_dec["vin"][0]["txid"] == self.watchonly_txid assert_greater_than(result["fee"], 0) - assert_greater_than(result["changepos"], -1) - assert_equal(result["fee"] + res_dec["vout"][result["changepos"]]["value"], self.watchonly_amount / 10) + assert_equal(result["changepos"], -1) + assert_equal(result["fee"] + res_dec["vout"][0]["value"], self.watchonly_amount) - signedtx = self.nodes[3].signrawtransactionwithwallet(result["hex"]) + signedtx = wwatch.signrawtransactionwithwallet(result["hex"]) assert not signedtx["complete"] signedtx = self.nodes[0].signrawtransactionwithwallet(signedtx["hex"]) assert signedtx["complete"] @@ -655,22 +704,91 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() - def test_option_feerate(self): - self.log.info("Test fundrawtxn feeRate option") + wwatch.unloadwallet() + def test_option_feerate(self): + self.log.info("Test fundrawtxn with explicit fee rates (fee_rate sat/vB and feeRate BTC/kvB)") + node = self.nodes[3] # Make sure there is exactly one input so coin selection can't skew the result. assert_equal(len(self.nodes[3].listunspent(1)), 1) - inputs = [] - outputs = {self.nodes[3].getnewaddress() : 1} - rawtx = self.nodes[3].createrawtransaction(inputs, outputs) - result = self.nodes[3].fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee) - result2 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}) - result3 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee}) - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[3].fundrawtransaction, rawtx, {"feeRate": 1}) + outputs = {node.getnewaddress() : 1} + rawtx = node.createrawtransaction(inputs, outputs) + + result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee) + btc_kvb_to_sat_vb = 100000 # (1e5) + result1 = node.fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee}) + result2 = node.fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}) + result3 = node.fundrawtransaction(rawtx, {"fee_rate": 10 * btc_kvb_to_sat_vb * self.min_relay_tx_fee}) + result4 = node.fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee}) + # Test that funding non-standard "zero-fee" transactions is valid. + result5 = self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 0}) + result6 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 0}) + result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex']) + assert_fee_amount(result1['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) + assert_fee_amount(result4['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) + assert_fee_amount(result5['fee'], count_bytes(result5['hex']), 0) + assert_fee_amount(result6['fee'], count_bytes(result6['hex']), 0) + + # With no arguments passed, expect fee of 141 satoshis. + assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001) + # Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified. + result = node.fundrawtransaction(rawtx, {"fee_rate": 10000}) + assert_approx(result["fee"], vexp=0.0141, vspan=0.0001) + + self.log.info("Test fundrawtxn with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + node.fundrawtransaction, rawtx, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True}) + for mode in ["", "foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', + node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True}) + + self.log.info("Test fundrawtxn with invalid conf_target settings") + for mode in ["unset", "economical", "conservative"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": v, "add_inputs": True}) + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h + node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": n, "add_inputs": True}) + + self.log.info("Test invalid fee rate settings") + for param, value in {("fee_rate", 100000), ("feeRate", 1.000)}: + assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", + node.fundrawtransaction, rawtx, {param: value, "add_inputs": True}) + assert_raises_rpc_error(-3, "Amount out of range", + node.fundrawtransaction, rawtx, {param: -1, "add_inputs": True}) + assert_raises_rpc_error(-3, "Amount is not a number or string", + node.fundrawtransaction, rawtx, {param: {"foo": "bar"}, "add_inputs": True}) + assert_raises_rpc_error(-3, "Invalid amount", + node.fundrawtransaction, rawtx, {param: "", "add_inputs": True}) + + self.log.info("Test min fee rate checks are bypassed with fundrawtxn, e.g. a fee_rate under 1 sat/vB is allowed") + node.fundrawtransaction(rawtx, {"fee_rate": 0.99999999, "add_inputs": True}) + node.fundrawtransaction(rawtx, {"feeRate": 0.00000999, "add_inputs": True}) + + self.log.info("- raises RPC error if both feeRate and fee_rate are passed") + assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)", + node.fundrawtransaction, rawtx, {"fee_rate": 0.1, "feeRate": 0.1, "add_inputs": True}) + + self.log.info("- raises RPC error if both feeRate and estimate_mode passed") + assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and feeRate", + node.fundrawtransaction, rawtx, {"estimate_mode": "economical", "feeRate": 0.1, "add_inputs": True}) + + for param in ["feeRate", "fee_rate"]: + self.log.info("- raises RPC error if both {} and conf_target are passed".format(param)) + assert_raises_rpc_error(-8, "Cannot specify both conf_target and {}. Please provide either a confirmation " + "target in blocks for automatic fee estimation, or an explicit fee rate.".format(param), + node.fundrawtransaction, rawtx, {param: 1, "conf_target": 1, "add_inputs": True}) + + self.log.info("- raises RPC error if both fee_rate and estimate_mode are passed") + assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate", + node.fundrawtransaction, rawtx, {"fee_rate": 1, "estimate_mode": "economical", "add_inputs": True}) def test_address_reuse(self): """Test no address reuse occurs.""" @@ -698,12 +816,32 @@ class RawTransactionsTest(BitcoinTestFramework): outputs = {self.nodes[2].getnewaddress(): 1} rawtx = self.nodes[3].createrawtransaction(inputs, outputs) + # Test subtract fee from outputs with feeRate (BTC/kvB) result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee) self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee) self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}), self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),] + dec_tx = [self.nodes[3].decoderawtransaction(tx_['hex']) for tx_ in result] + output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)] + change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)] + assert_equal(result[0]['fee'], result[1]['fee'], result[2]['fee']) + assert_equal(result[3]['fee'], result[4]['fee']) + assert_equal(change[0], change[1]) + assert_equal(output[0], output[1]) + assert_equal(output[0], output[2] + result[2]['fee']) + assert_equal(change[0] + result[0]['fee'], change[2]) + assert_equal(output[3], output[4] + result[4]['fee']) + assert_equal(change[3] + result[3]['fee'], change[4]) + + # Test subtract fee from outputs with fee_rate (sat/vB) + btc_kvb_to_sat_vb = 100000 # (1e5) + result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee) + self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list + self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee) + self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee}), + self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),] dec_tx = [self.nodes[3].decoderawtransaction(tx_['hex']) for tx_ in result] output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)] change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)] diff --git a/test/functional/rpc_getblockfilter.py b/test/functional/rpc_getblockfilter.py index 8fa36445cd..c3c3622cf9 100755 --- a/test/functional/rpc_getblockfilter.py +++ b/test/functional/rpc_getblockfilter.py @@ -7,7 +7,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_is_hex_string, assert_raises_rpc_error, - connect_nodes, disconnect_nodes ) FILTER_TYPES = ["basic"] @@ -20,7 +19,7 @@ class GetBlockFilterTest(BitcoinTestFramework): def run_test(self): # Create two chains by disconnecting nodes 0 & 1, mining, then reconnecting - disconnect_nodes(self.nodes[0], 1) + self.disconnect_nodes(0, 1) self.nodes[0].generate(3) self.nodes[1].generate(4) @@ -29,7 +28,7 @@ class GetBlockFilterTest(BitcoinTestFramework): chain0_hashes = [self.nodes[0].getblockhash(block_height) for block_height in range(4)] # Reorg node 0 to a new chain - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks() assert_equal(self.nodes[0].getblockcount(), 4) diff --git a/test/functional/rpc_getdescriptorinfo.py b/test/functional/rpc_getdescriptorinfo.py index 977dc805ef..ea064f9763 100755 --- a/test/functional/rpc_getdescriptorinfo.py +++ b/test/functional/rpc_getdescriptorinfo.py @@ -17,6 +17,7 @@ class DescriptorTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [["-disablewallet"]] + self.wallet_names = [] def test_desc(self, desc, isrange, issolvable, hasprivatekeys): info = self.nodes[0].getdescriptorinfo(desc) diff --git a/test/functional/rpc_getpeerinfo_deprecation.py b/test/functional/rpc_getpeerinfo_deprecation.py index 287c40ae3e..340a66e12f 100755 --- a/test/functional/rpc_getpeerinfo_deprecation.py +++ b/test/functional/rpc_getpeerinfo_deprecation.py @@ -5,7 +5,6 @@ """Test deprecation of getpeerinfo RPC fields.""" from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import connect_nodes class GetpeerinfoDeprecationTest(BitcoinTestFramework): @@ -26,7 +25,7 @@ class GetpeerinfoDeprecationTest(BitcoinTestFramework): def test_addnode_deprecation(self): self.restart_node(1, ["-deprecatedrpc=getpeerinfo_addnode"]) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.log.info("Test getpeerinfo by default no longer returns an addnode field") assert "addnode" not in self.nodes[0].getpeerinfo()[0].keys() diff --git a/test/functional/rpc_invalidateblock.py b/test/functional/rpc_invalidateblock.py index e788e75557..f884b8d293 100755 --- a/test/functional/rpc_invalidateblock.py +++ b/test/functional/rpc_invalidateblock.py @@ -8,7 +8,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR from test_framework.util import ( assert_equal, - connect_nodes, ) @@ -32,7 +31,7 @@ class InvalidateTest(BitcoinTestFramework): assert_equal(self.nodes[1].getblockcount(), 6) self.log.info("Connect nodes to force a reorg") - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks(self.nodes[0:2]) assert_equal(self.nodes[0].getblockcount(), 6) badhash = self.nodes[1].getblockhash(2) @@ -43,7 +42,7 @@ class InvalidateTest(BitcoinTestFramework): assert_equal(self.nodes[0].getbestblockhash(), besthash_n0) self.log.info("Make sure we won't reorg to a lower work chain:") - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) self.log.info("Sync node 2 to node 1 so both have 6 blocks") self.sync_blocks(self.nodes[1:3]) assert_equal(self.nodes[2].getblockcount(), 6) diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index b8a04f494d..2efd090733 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -21,10 +21,8 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, assert_equal, - assert_greater_than_or_equal, assert_greater_than, assert_raises_rpc_error, - connect_nodes, p2p_port, ) @@ -52,9 +50,13 @@ class NetTest(BitcoinTestFramework): def run_test(self): # Get out of IBD for the minfeefilter and getpeerinfo tests. self.nodes[0].generate(101) - # Connect nodes both ways. - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 0) + + # By default, the test framework sets up an addnode connection from + # node 1 --> node0. By connecting node0 --> node 1, we're left with + # the two nodes being connected both ways. + # Topology will look like: node0 <--> node1 + self.connect_nodes(0, 1) + self.sync_all() self.test_connection_count() self.test_getpeerinfo() @@ -69,36 +71,52 @@ class NetTest(BitcoinTestFramework): # After using `connect_nodes` to connect nodes 0 and 1 to each other. assert_equal(self.nodes[0].getconnectioncount(), 2) + def test_getpeerinfo(self): + self.log.info("Test getpeerinfo") + # Create a few getpeerinfo last_block/last_transaction values. + if self.is_wallet_compiled(): + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) + self.nodes[1].generate(1) + self.sync_all() + time_now = int(time.time()) + peer_info = [x.getpeerinfo() for x in self.nodes] + # Verify last_block and last_transaction keys/values. + for node, peer, field in product(range(self.num_nodes), range(2), ['last_block', 'last_transaction']): + assert field in peer_info[node][peer].keys() + if peer_info[node][peer][field] != 0: + assert_approx(peer_info[node][peer][field], time_now, vspan=60) + # check both sides of bidirectional connection between nodes + # the address bound to on one side will be the source address for the other node + assert_equal(peer_info[0][0]['addrbind'], peer_info[1][0]['addr']) + assert_equal(peer_info[1][0]['addrbind'], peer_info[0][0]['addr']) + assert_equal(peer_info[0][0]['minfeefilter'], Decimal("0.00000500")) + assert_equal(peer_info[1][0]['minfeefilter'], Decimal("0.00001000")) + # check the `servicesnames` field + for info in peer_info: + assert_net_servicesnames(int(info[0]["services"], 0x10), info[0]["servicesnames"]) + + assert_equal(peer_info[0][0]['connection_type'], 'inbound') + assert_equal(peer_info[0][1]['connection_type'], 'manual') + + assert_equal(peer_info[1][0]['connection_type'], 'manual') + assert_equal(peer_info[1][1]['connection_type'], 'inbound') + def test_getnettotals(self): self.log.info("Test getnettotals") - # getnettotals totalbytesrecv and totalbytessent should be - # consistent with getpeerinfo. Since the RPC calls are not atomic, - # and messages might have been recvd or sent between RPC calls, call - # getnettotals before and after and verify that the returned values - # from getpeerinfo are bounded by those values. + # Test getnettotals and getpeerinfo by doing a ping. The bytes + # sent/received should increase by at least the size of one ping (32 + # bytes) and one pong (32 bytes). net_totals_before = self.nodes[0].getnettotals() - peer_info = self.nodes[0].getpeerinfo() - net_totals_after = self.nodes[0].getnettotals() - assert_equal(len(peer_info), 2) - peers_recv = sum([peer['bytesrecv'] for peer in peer_info]) - peers_sent = sum([peer['bytessent'] for peer in peer_info]) - - assert_greater_than_or_equal(peers_recv, net_totals_before['totalbytesrecv']) - assert_greater_than_or_equal(net_totals_after['totalbytesrecv'], peers_recv) - assert_greater_than_or_equal(peers_sent, net_totals_before['totalbytessent']) - assert_greater_than_or_equal(net_totals_after['totalbytessent'], peers_sent) - - # test getnettotals and getpeerinfo by doing a ping - # the bytes sent/received should change - # note ping and pong are 32 bytes each + peer_info_before = self.nodes[0].getpeerinfo() + self.nodes[0].ping() - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_after['totalbytessent'] + 32 * 2), timeout=1) - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_after['totalbytesrecv'] + 32 * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_before['totalbytessent'] + 32 * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_before['totalbytesrecv'] + 32 * 2), timeout=1) - peer_info_after_ping = self.nodes[0].getpeerinfo() - for before, after in zip(peer_info, peer_info_after_ping): - assert_greater_than_or_equal(after['bytesrecv_per_msg'].get('pong', 0), before['bytesrecv_per_msg'].get('pong', 0) + 32) - assert_greater_than_or_equal(after['bytessent_per_msg'].get('ping', 0), before['bytessent_per_msg'].get('ping', 0) + 32) + for peer_before in peer_info_before: + peer_after = lambda: next(p for p in self.nodes[0].getpeerinfo() if p['id'] == peer_before['id']) + self.wait_until(lambda: peer_after()['bytesrecv_per_msg'].get('pong', 0) >= peer_before['bytesrecv_per_msg'].get('pong', 0) + 32, timeout=1) + self.wait_until(lambda: peer_after()['bytessent_per_msg'].get('ping', 0) >= peer_before['bytessent_per_msg'].get('ping', 0) + 32, timeout=1) def test_getnetworkinfo(self): self.log.info("Test getnetworkinfo") @@ -117,8 +135,8 @@ class NetTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']): self.nodes[0].setnetworkactive(state=True) # Connect nodes both ways. - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 0) + self.connect_nodes(0, 1) + self.connect_nodes(1, 0) info = self.nodes[0].getnetworkinfo() assert_equal(info['networkactive'], True) @@ -151,36 +169,6 @@ class NetTest(BitcoinTestFramework): # check that a non-existent node returns an error assert_raises_rpc_error(-24, "Node has not been added", self.nodes[0].getaddednodeinfo, '1.1.1.1') - def test_getpeerinfo(self): - self.log.info("Test getpeerinfo") - # Create a few getpeerinfo last_block/last_transaction values. - if self.is_wallet_compiled(): - self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) - self.nodes[1].generate(1) - self.sync_all() - time_now = int(time.time()) - peer_info = [x.getpeerinfo() for x in self.nodes] - # Verify last_block and last_transaction keys/values. - for node, peer, field in product(range(self.num_nodes), range(2), ['last_block', 'last_transaction']): - assert field in peer_info[node][peer].keys() - if peer_info[node][peer][field] != 0: - assert_approx(peer_info[node][peer][field], time_now, vspan=60) - # check both sides of bidirectional connection between nodes - # the address bound to on one side will be the source address for the other node - assert_equal(peer_info[0][0]['addrbind'], peer_info[1][0]['addr']) - assert_equal(peer_info[1][0]['addrbind'], peer_info[0][0]['addr']) - assert_equal(peer_info[0][0]['minfeefilter'], Decimal("0.00000500")) - assert_equal(peer_info[1][0]['minfeefilter'], Decimal("0.00001000")) - # check the `servicesnames` field - for info in peer_info: - assert_net_servicesnames(int(info[0]["services"], 0x10), info[0]["servicesnames"]) - - assert_equal(peer_info[0][0]['connection_type'], 'inbound') - assert_equal(peer_info[0][1]['connection_type'], 'manual') - - assert_equal(peer_info[1][0]['connection_type'], 'manual') - assert_equal(peer_info[1][1]['connection_type'], 'inbound') - def test_service_flags(self): self.log.info("Test service flags") self.nodes[0].add_p2p_connection(P2PInterface(), services=(1 << 4) | (1 << 63)) diff --git a/test/functional/rpc_preciousblock.py b/test/functional/rpc_preciousblock.py index 8386e47411..04d55b103f 100755 --- a/test/functional/rpc_preciousblock.py +++ b/test/functional/rpc_preciousblock.py @@ -7,7 +7,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, ) def unidirectional_node_sync_via_rpc(node_src, node_dest): @@ -61,7 +60,7 @@ class PreciousTest(BitcoinTestFramework): self.log.info("Connect nodes and check no reorg occurs") # Submit competing blocks via RPC so any reorg should occur before we proceed (no way to wait on inaction for p2p sync) node_sync_via_rpc(self.nodes[0:2]) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) assert_equal(self.nodes[0].getbestblockhash(), hashC) assert_equal(self.nodes[1].getbestblockhash(), hashG) self.log.info("Make Node0 prefer block G") @@ -98,8 +97,8 @@ class PreciousTest(BitcoinTestFramework): hashL = self.nodes[2].getbestblockhash() self.log.info("Connect nodes and check no reorg occurs") node_sync_via_rpc(self.nodes[1:3]) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) assert_equal(self.nodes[0].getbestblockhash(), hashH) assert_equal(self.nodes[1].getbestblockhash(), hashH) assert_equal(self.nodes[2].getbestblockhash(), hashL) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 781a49dfac..5840801b00 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -12,8 +12,6 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - connect_nodes, - disconnect_nodes, find_output, ) @@ -30,7 +28,7 @@ class PSBTTest(BitcoinTestFramework): self.num_nodes = 3 self.extra_args = [ ["-walletrbf=1"], - ["-walletrbf=0"], + ["-walletrbf=0", "-changetype=legacy"], [] ] self.supports_cli = False @@ -46,7 +44,7 @@ class PSBTTest(BitcoinTestFramework): # Disconnect offline node from others # Topology of test network is linear, so this one call is enough - disconnect_nodes(offline_node, 1) + self.disconnect_nodes(0, 1) # Create watchonly on online_node online_node.createwallet(wallet_name='wonline', disable_private_keys=True) @@ -80,8 +78,16 @@ class PSBTTest(BitcoinTestFramework): wonline.unloadwallet() # Reconnect - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + + def assert_change_type(self, psbtx, expected_type): + """Assert that the given PSBT has a change output with the given type.""" + + # The decodepsbt RPC is stateless and independent of any settings, we can always just call it on the first node + decoded_psbt = self.nodes[0].decodepsbt(psbtx["psbt"]) + changepos = psbtx["changepos"] + assert_equal(decoded_psbt["tx"]["vout"][changepos]["scriptPubKey"]["type"], expected_type) def run_test(self): # Create and fund a raw tx for sending 10 BTC @@ -166,8 +172,11 @@ class PSBTTest(BitcoinTestFramework): elif out['scriptPubKey']['addresses'][0] == p2pkh: p2pkh_pos = out['n'] + inputs = [{"txid": txid, "vout": p2wpkh_pos}, {"txid": txid, "vout": p2sh_p2wpkh_pos}, {"txid": txid, "vout": p2pkh_pos}] + outputs = [{self.nodes[1].getnewaddress(): 29.99}] + # spend single key from node 1 - created_psbt = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}) + created_psbt = self.nodes[1].walletcreatefundedpsbt(inputs, outputs) walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(created_psbt['psbt']) # Make sure it has both types of UTXOs decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) @@ -178,15 +187,77 @@ class PSBTTest(BitcoinTestFramework): assert_equal(walletprocesspsbt_out['complete'], True) self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) - # feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000): - res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1, "add_inputs": True}) - assert_approx(res["fee"], 0.055, 0.005) - - # feeRate of 10 BTC / KB produces a total fee well above -maxtxfee + self.log.info("Test walletcreatefundedpsbt fee rate of 10000 sat/vB and 0.1 BTC/kvB produces a total fee at or slightly below -maxtxfee (~0.05290000)") + res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True}) + assert_approx(res1["fee"], 0.055, 0.005) + res2 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True}) + assert_approx(res2["fee"], 0.055, 0.005) + + self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed, e.g. a fee_rate under 1 sat/vB is allowed") + res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 0.99999999, "add_inputs": True}) + assert_approx(res3["fee"], 0.00000381, 0.0000001) + res4 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.00000999, "add_inputs": True}) + assert_approx(res4["fee"], 0.00000381, 0.0000001) + + self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed and that funding non-standard 'zero-fee' transactions is valid") + for param in ["fee_rate", "feeRate"]: + assert_equal(self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: 0, "add_inputs": True})["fee"], 0) + + self.log.info("Test invalid fee rate settings") + for param, value in {("fee_rate", 100000), ("feeRate", 1)}: + assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: value, "add_inputs": True}) + assert_raises_rpc_error(-3, "Amount out of range", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: -1, "add_inputs": True}) + assert_raises_rpc_error(-3, "Amount is not a number or string", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: {"foo": "bar"}, "add_inputs": True}) + assert_raises_rpc_error(-3, "Invalid amount", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: "", "add_inputs": True}) + + self.log.info("- raises RPC error if both feeRate and fee_rate are passed") + assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0.1, "feeRate": 0.1, "add_inputs": True}) + + self.log.info("- raises RPC error if both feeRate and estimate_mode passed") + assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and feeRate", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": "economical", "feeRate": 0.1, "add_inputs": True}) + + for param in ["feeRate", "fee_rate"]: + self.log.info("- raises RPC error if both {} and conf_target are passed".format(param)) + assert_raises_rpc_error(-8, "Cannot specify both conf_target and {}. Please provide either a confirmation " + "target in blocks for automatic fee estimation, or an explicit fee rate.".format(param), + self.nodes[1].walletcreatefundedpsbt ,inputs, outputs, 0, {param: 1, "conf_target": 1, "add_inputs": True}) + + self.log.info("- raises RPC error if both fee_rate and estimate_mode are passed") + assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate", + self.nodes[1].walletcreatefundedpsbt ,inputs, outputs, 0, {"fee_rate": 1, "estimate_mode": "economical", "add_inputs": True}) + + self.log.info("- raises RPC error with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True}) + for mode in ["", "foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True}) + + self.log.info("- raises RPC error with invalid conf_target settings") + for mode in ["unset", "economical", "conservative"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True}) + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True}) + + self.log.info("Test walletcreatefundedpsbt with too-high fee rate produces total fee well above -maxtxfee and raises RPC error") # previously this was silently capped at -maxtxfee - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10, "add_inputs": True}) - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": False}) + for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items(): + msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)" + assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"fee_rate": 1000000, "add_inputs": bool_add}) + assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add}) + self.log.info("Test various PSBT operations") # partially sign multisig things with node 1 psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs={self.nodes[1].getnewaddress():29.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx) @@ -301,6 +372,21 @@ class PSBTTest(BitcoinTestFramework): # when attempting BnB coin selection self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"changeAddress":self.nodes[1].getnewaddress()}, False) + # Make sure the wallet's change type is respected by default + small_output = {self.nodes[0].getnewaddress():0.1} + psbtx_native = self.nodes[0].walletcreatefundedpsbt([], [small_output]) + self.assert_change_type(psbtx_native, "witness_v0_keyhash") + psbtx_legacy = self.nodes[1].walletcreatefundedpsbt([], [small_output]) + self.assert_change_type(psbtx_legacy, "pubkeyhash") + + # Make sure the change type of the wallet can also be overwritten + psbtx_np2wkh = self.nodes[1].walletcreatefundedpsbt([], [small_output], 0, {"change_type":"p2sh-segwit"}) + self.assert_change_type(psbtx_np2wkh, "scripthash") + + # Make sure the change type cannot be specified if a change address is given + invalid_options = {"change_type":"legacy","changeAddress":self.nodes[0].getnewaddress()} + assert_raises_rpc_error(-8, "both change address and address type options", self.nodes[0].walletcreatefundedpsbt, [], [small_output], 0, invalid_options) + # Regression test for 14473 (mishandling of already-signed witness transaction): psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], 0, {"add_inputs": True}) complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"]) diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 23b5e647d6..554c30c0d2 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -20,7 +20,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, + find_vout_for_address, hex_str_to_bytes, ) @@ -60,7 +60,7 @@ class RawTransactionsTest(BitcoinTestFramework): def setup_network(self): super().setup_network() - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) def run_test(self): self.log.info('prepare some coins for multiple *rawtransaction commands') @@ -96,7 +96,7 @@ class RawTransactionsTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "txid must be hexadecimal string (not 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844')", self.nodes[0].createrawtransaction, [{'txid': 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844'}], {}) assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid}], {}) assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 'foo'}], {}) - assert_raises_rpc_error(-8, "Invalid parameter, vout must be positive", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': -1}], {}) + assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': -1}], {}) assert_raises_rpc_error(-8, "Invalid parameter, sequence number is out of range", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 0, 'sequence': -1}], {}) # Test `createrawtransaction` invalid `outputs` @@ -243,121 +243,124 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].reconsiderblock(block1) assert_equal(self.nodes[0].getbestblockhash(), block2) - ######################### - # RAW TX MULTISIG TESTS # - ######################### - # 2of2 test - addr1 = self.nodes[2].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[2].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - # Tests for createmultisig and addmultisigaddress - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) - self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here. - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] - - #use balance deltas instead of absolute values - bal = self.nodes[2].getbalance() - - # send 1.2 BTC to msig adr - txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance - - - # 2of3 test from different nodes - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - addr3 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - addr3Obj = self.nodes[2].getaddressinfo(addr3) - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - #THIS IS AN INCOMPLETE FEATURE - #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION - assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys - self.nodes[2].sendrawtransaction(rawTxSigned['hex']) - rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx - - # 2of2 test for combining transactions - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned1) - assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned2) - assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx - rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) - self.log.debug(rawTxComb) - self.nodes[2].sendrawtransaction(rawTxComb) - rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx + if not self.options.descriptors: + # The traditional multisig workflow does not work with descriptor wallets so these are legacy only. + # The multisig workflow with descriptor wallets uses PSBTs and is tested elsewhere, no need to do them here. + ######################### + # RAW TX MULTISIG TESTS # + ######################### + # 2of2 test + addr1 = self.nodes[2].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[2].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + # Tests for createmultisig and addmultisigaddress + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) + self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here. + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] + + #use balance deltas instead of absolute values + bal = self.nodes[2].getbalance() + + # send 1.2 BTC to msig adr + txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance + + + # 2of3 test from different nodes + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + addr3 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + addr3Obj = self.nodes[2].getaddressinfo(addr3) + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + #THIS IS AN INCOMPLETE FEATURE + #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION + assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}] + outputs = { self.nodes[0].getnewaddress() : 2.19 } + rawTx = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx + + rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys + self.nodes[2].sendrawtransaction(rawTxSigned['hex']) + rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx + + # 2of2 test for combining transactions + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}] + outputs = { self.nodes[0].getnewaddress() : 2.19 } + rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned1) + assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx + + rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned2) + assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx + rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) + self.log.debug(rawTxComb) + self.nodes[2].sendrawtransaction(rawTxComb) + rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx # decoderawtransaction tests # witness transaction @@ -370,9 +373,20 @@ class RawTransactionsTest(BitcoinTestFramework): decrawtx = self.nodes[0].decoderawtransaction(encrawtx, False) # decode as non-witness transaction assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) + # Basic signrawtransaction test + addr = self.nodes[1].getnewaddress() + txid = self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].generate(1) + self.sync_all() + vout = find_vout_for_address(self.nodes[1], txid, addr) + rawTx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {self.nodes[1].getnewaddress(): 9.999}) + rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx) + txId = self.nodes[1].sendrawtransaction(rawTxSigned['hex']) + self.nodes[0].generate(1) + self.sync_all() + # getrawtransaction tests # 1. valid parameters - only supply txid - txId = rawTx["txid"] assert_equal(self.nodes[0].getrawtransaction(txId), rawTxSigned['hex']) # 2. valid parameters - supply txid and 0 for non-verbose @@ -456,9 +470,9 @@ class RawTransactionsTest(BitcoinTestFramework): # Thus, testmempoolaccept should reject testres = self.nodes[2].testmempoolaccept([rawTxSigned['hex']], 0.00001000)[0] assert_equal(testres['allowed'], False) - assert_equal(testres['reject-reason'], 'absurdly-high-fee') + assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw - assert_raises_rpc_error(-26, "absurdly-high-fee", self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000) + assert_raises_rpc_error(-25, 'Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)', self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000) # and the following calls should both succeed testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']])[0] assert_equal(testres['allowed'], True) @@ -480,9 +494,9 @@ class RawTransactionsTest(BitcoinTestFramework): # Thus, testmempoolaccept should reject testres = self.nodes[2].testmempoolaccept([rawTxSigned['hex']])[0] assert_equal(testres['allowed'], False) - assert_equal(testres['reject-reason'], 'absurdly-high-fee') + assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw - assert_raises_rpc_error(-26, "absurdly-high-fee", self.nodes[2].sendrawtransaction, rawTxSigned['hex']) + assert_raises_rpc_error(-25, 'Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)', self.nodes[2].sendrawtransaction, rawTxSigned['hex']) # and the following calls should both succeed testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']], maxfeerate='0.20000000')[0] assert_equal(testres['allowed'], True) diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 861b394e70..070f59d314 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -55,7 +55,8 @@ class ScantxoutsetTest(BitcoinTestFramework): self.log.info("Stop node, remove wallet, mine again some blocks...") self.stop_node(0) shutil.rmtree(os.path.join(self.nodes[0].datadir, self.chain, 'wallets')) - self.start_node(0) + self.start_node(0, ['-nowallet']) + self.import_deterministic_coinbase_privkeys() self.nodes[0].generate(110) scan = self.nodes[0].scantxoutset("start", []) diff --git a/test/functional/rpc_setban.py b/test/functional/rpc_setban.py index 1cc1fb164b..bc48449084 100755 --- a/test/functional/rpc_setban.py +++ b/test/functional/rpc_setban.py @@ -6,7 +6,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( - connect_nodes, p2p_port ) @@ -18,7 +17,7 @@ class SetBanTests(BitcoinTestFramework): def run_test(self): # Node 0 connects to Node 1, check that the noban permission is not granted - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] assert(not 'noban' in peerinfo['permissions']) @@ -32,14 +31,14 @@ class SetBanTests(BitcoinTestFramework): # However, node 0 should be able to reconnect if it has noban permission self.restart_node(1, ['-whitelist=127.0.0.1']) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] assert('noban' in peerinfo['permissions']) # If we remove the ban, Node 0 should be able to reconnect even without noban permission self.nodes[1].setban("127.0.0.1", "remove") self.restart_node(1, []) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] assert(not 'noban' in peerinfo['permissions']) diff --git a/test/functional/rpc_signrawtransaction.py b/test/functional/rpc_signrawtransaction.py index 704b65c060..2fbbdbbdf0 100755 --- a/test/functional/rpc_signrawtransaction.py +++ b/test/functional/rpc_signrawtransaction.py @@ -5,10 +5,13 @@ """Test transaction signing using the signrawtransaction* RPCs.""" from test_framework.address import check_script, script_to_p2sh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, find_vout_for_address, hex_str_to_bytes from test_framework.messages import sha256 from test_framework.script import CScript, OP_0, OP_CHECKSIG +from test_framework.script_util import key_to_p2pkh_script, script_to_p2sh_p2wsh_script, script_to_p2wsh_script +from test_framework.wallet_util import bytes_to_wif from decimal import Decimal @@ -148,24 +151,40 @@ class SignRawTransactionsTest(BitcoinTestFramework): assert_equal(rawTxSigned['errors'][1]['witness'], ["304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee01", "025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357"]) assert not rawTxSigned['errors'][0]['witness'] + def test_fully_signed_tx(self): + self.log.info("Test signing a fully signed transaction does nothing") + self.nodes[0].walletpassphrase("password", 9999) + self.nodes[0].generate(101) + rawtx = self.nodes[0].createrawtransaction([], [{self.nodes[0].getnewaddress(): 10}]) + fundedtx = self.nodes[0].fundrawtransaction(rawtx) + signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx["hex"]) + assert_equal(signedtx["complete"], True) + signedtx2 = self.nodes[0].signrawtransactionwithwallet(signedtx["hex"]) + assert_equal(signedtx2["complete"], True) + assert_equal(signedtx["hex"], signedtx2["hex"]) + self.nodes[0].walletlock() + def witness_script_test(self): self.log.info("Test signing transaction to P2SH-P2WSH addresses without wallet") # Create a new P2SH-P2WSH 1-of-1 multisig address: - embedded_address = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress()) - embedded_privkey = self.nodes[1].dumpprivkey(embedded_address["address"]) - p2sh_p2wsh_address = self.nodes[1].addmultisigaddress(1, [embedded_address["pubkey"]], "", "p2sh-segwit") + eckey = ECKey() + eckey.generate() + embedded_privkey = bytes_to_wif(eckey.get_bytes()) + embedded_pubkey = eckey.get_pubkey().get_bytes().hex() + p2sh_p2wsh_address = self.nodes[1].createmultisig(1, [embedded_pubkey], "p2sh-segwit") # send transaction to P2SH-P2WSH 1-of-1 multisig address self.nodes[0].generate(101) self.nodes[0].sendtoaddress(p2sh_p2wsh_address["address"], 49.999) self.nodes[0].generate(1) self.sync_all() - # Find the UTXO for the transaction node[1] should have received, check witnessScript matches - unspent_output = self.nodes[1].listunspent(0, 999999, [p2sh_p2wsh_address["address"]])[0] - assert_equal(unspent_output["witnessScript"], p2sh_p2wsh_address["redeemScript"]) - p2sh_redeemScript = CScript([OP_0, sha256(hex_str_to_bytes(p2sh_p2wsh_address["redeemScript"]))]) - assert_equal(unspent_output["redeemScript"], p2sh_redeemScript.hex()) + # Get the UTXO info from scantxoutset + unspent_output = self.nodes[1].scantxoutset('start', [p2sh_p2wsh_address['descriptor']])['unspents'][0] + spk = script_to_p2sh_p2wsh_script(p2sh_p2wsh_address['redeemScript']).hex() + unspent_output['witnessScript'] = p2sh_p2wsh_address['redeemScript'] + unspent_output['redeemScript'] = script_to_p2wsh_script(unspent_output['witnessScript']).hex() + assert_equal(spk, unspent_output['scriptPubKey']) # Now create and sign a transaction spending that output on node[0], which doesn't know the scripts or keys - spending_tx = self.nodes[0].createrawtransaction([unspent_output], {self.nodes[1].getnewaddress(): Decimal("49.998")}) + spending_tx = self.nodes[0].createrawtransaction([unspent_output], {self.nodes[1].get_wallet_rpc(self.default_wallet_name).getnewaddress(): Decimal("49.998")}) spending_tx_signed = self.nodes[0].signrawtransactionwithkey(spending_tx, [embedded_privkey], [unspent_output]) # Check the signing completed successfully assert 'complete' in spending_tx_signed @@ -177,11 +196,13 @@ class SignRawTransactionsTest(BitcoinTestFramework): def verify_txn_with_witness_script(self, tx_type): self.log.info("Test with a {} script as the witnessScript".format(tx_type)) - embedded_addr_info = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress('', 'legacy')) - embedded_privkey = self.nodes[1].dumpprivkey(embedded_addr_info['address']) + eckey = ECKey() + eckey.generate() + embedded_privkey = bytes_to_wif(eckey.get_bytes()) + embedded_pubkey = eckey.get_pubkey().get_bytes().hex() witness_script = { - 'P2PKH': embedded_addr_info['scriptPubKey'], - 'P2PK': CScript([hex_str_to_bytes(embedded_addr_info['pubkey']), OP_CHECKSIG]).hex() + 'P2PKH': key_to_p2pkh_script(embedded_pubkey).hex(), + 'P2PK': CScript([hex_str_to_bytes(embedded_pubkey), OP_CHECKSIG]).hex() }.get(tx_type, "Invalid tx_type") redeem_script = CScript([OP_0, sha256(check_script(witness_script))]).hex() addr = script_to_p2sh(redeem_script) @@ -223,6 +244,7 @@ class SignRawTransactionsTest(BitcoinTestFramework): self.witness_script_test() self.OP_1NEGATE_test() self.test_with_lock_outputs() + self.test_fully_signed_tx() if __name__ == '__main__': diff --git a/test/functional/rpc_txoutproof.py b/test/functional/rpc_txoutproof.py index 93fb62c5d6..2d6ce77613 100755 --- a/test/functional/rpc_txoutproof.py +++ b/test/functional/rpc_txoutproof.py @@ -78,7 +78,7 @@ class MerkleBlockTest(BitcoinTestFramework): # We can't get a proof if we specify transactions from different blocks assert_raises_rpc_error(-5, "Not all transactions found in specified or retrieved block", self.nodes[0].gettxoutproof, [txid1, txid3]) # Test empty list - assert_raises_rpc_error(-5, "Transaction not yet in block", self.nodes[0].gettxoutproof, []) + assert_raises_rpc_error(-8, "Parameter 'txids' cannot be empty", self.nodes[0].gettxoutproof, []) # Test duplicate txid assert_raises_rpc_error(-8, 'Invalid parameter, duplicated txid', self.nodes[0].gettxoutproof, [txid1, txid1]) diff --git a/test/functional/rpc_users.py b/test/functional/rpc_users.py index daf02fc4f3..108af2cac8 100755 --- a/test/functional/rpc_users.py +++ b/test/functional/rpc_users.py @@ -99,11 +99,18 @@ class HTTPBasicsTest(BitcoinTestFramework): self.test_auth(self.nodes[1], self.rpcuser, self.rpcpassword) - self.log.info('Check that failure to write cookie file will abort the node gracefully') + init_error = 'Error: Unable to start HTTP server. See debug log for details.' + + self.log.info('Check -rpcauth are validated') + # Empty -rpcauth= are ignored + self.restart_node(0, extra_args=['-rpcauth=']) self.stop_node(0) + self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo']) + self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar']) + + self.log.info('Check that failure to write cookie file will abort the node gracefully') cookie_file = os.path.join(get_datadir_path(self.options.tmpdir, 0), self.chain, '.cookie.tmp') os.mkdir(cookie_file) - init_error = 'Error: Unable to start HTTP server. See debug log for details.' self.nodes[0].assert_start_raises_init_error(expected_msg=init_error) diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py index 9506b63f82..360962b8da 100644 --- a/test/functional/test_framework/address.py +++ b/test/functional/test_framework/address.py @@ -2,17 +2,17 @@ # Copyright (c) 2016-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. -"""Encode and decode BASE58, P2PKH and P2SH addresses.""" +"""Encode and decode Bitcoin addresses. + +- base58 P2PKH and P2SH addresses. +- bech32 segwit v0 P2WPKH and P2WSH addresses.""" import enum import unittest from .script import hash256, hash160, sha256, CScript, OP_0 -from .util import hex_str_to_bytes - -from . import segwit_addr - -from test_framework.util import assert_equal +from .segwit_addr import encode_segwit_address +from .util import assert_equal, hex_str_to_bytes ADDRESS_BCRT1_UNSPENDABLE = 'bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj' ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR = 'addr(bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj)#juyq9d97' @@ -35,7 +35,7 @@ def byte_to_base58(b, version): str = chr(version).encode('latin-1').hex() + str checksum = hash256(hex_str_to_bytes(str)).hex() str += checksum[:8] - value = int('0x'+str,0) + value = int('0x' + str, 0) while value > 0: result = chars[value % 58] + result value //= 58 @@ -45,7 +45,10 @@ def byte_to_base58(b, version): return result -def base58_to_byte(s, verify_checksum=True): +def base58_to_byte(s): + """Converts a base58-encoded string to its data and version. + + Throws if the base58 checksum is invalid.""" if not s: return b'' n = 0 @@ -65,66 +68,67 @@ def base58_to_byte(s, verify_checksum=True): else: break res = b'\x00' * pad + res - if verify_checksum: - assert_equal(hash256(res[:-4])[:4], res[-4:]) + + # Assert if the checksum is invalid + assert_equal(hash256(res[:-4])[:4], res[-4:]) return res[1:-4], int(res[0]) -def keyhash_to_p2pkh(hash, main = False): +def keyhash_to_p2pkh(hash, main=False): assert len(hash) == 20 version = 0 if main else 111 return byte_to_base58(hash, version) -def scripthash_to_p2sh(hash, main = False): +def scripthash_to_p2sh(hash, main=False): assert len(hash) == 20 version = 5 if main else 196 return byte_to_base58(hash, version) -def key_to_p2pkh(key, main = False): +def key_to_p2pkh(key, main=False): key = check_key(key) return keyhash_to_p2pkh(hash160(key), main) -def script_to_p2sh(script, main = False): +def script_to_p2sh(script, main=False): script = check_script(script) return scripthash_to_p2sh(hash160(script), main) -def key_to_p2sh_p2wpkh(key, main = False): +def key_to_p2sh_p2wpkh(key, main=False): key = check_key(key) p2shscript = CScript([OP_0, hash160(key)]) return script_to_p2sh(p2shscript, main) -def program_to_witness(version, program, main = False): +def program_to_witness(version, program, main=False): if (type(program) is str): program = hex_str_to_bytes(program) assert 0 <= version <= 16 assert 2 <= len(program) <= 40 assert version > 0 or len(program) in [20, 32] - return segwit_addr.encode("bc" if main else "bcrt", version, program) + return encode_segwit_address("bc" if main else "bcrt", version, program) -def script_to_p2wsh(script, main = False): +def script_to_p2wsh(script, main=False): script = check_script(script) return program_to_witness(0, sha256(script), main) -def key_to_p2wpkh(key, main = False): +def key_to_p2wpkh(key, main=False): key = check_key(key) return program_to_witness(0, hash160(key), main) -def script_to_p2sh_p2wsh(script, main = False): +def script_to_p2sh_p2wsh(script, main=False): script = check_script(script) p2shscript = CScript([OP_0, sha256(script)]) return script_to_p2sh(p2shscript, main) def check_key(key): if (type(key) is str): - key = hex_str_to_bytes(key) # Assuming this is hex string + key = hex_str_to_bytes(key) # Assuming this is hex string if (type(key) is bytes and (len(key) == 33 or len(key) == 65)): return key assert False def check_script(script): if (type(script) is str): - script = hex_str_to_bytes(script) # Assuming this is hex string + script = hex_str_to_bytes(script) # Assuming this is hex string if (type(script) is bytes or type(script) is CScript): return script assert False @@ -135,15 +139,15 @@ class TestFrameworkScript(unittest.TestCase): def check_base58(data, version): self.assertEqual(base58_to_byte(byte_to_base58(data, version)), (data, version)) - check_base58(b'\x1f\x8e\xa1p*{\xd4\x94\x1b\xca\tA\xb8R\xc4\xbb\xfe\xdb.\x05', 111) - check_base58(b':\x0b\x05\xf4\xd7\xf6l;\xa7\x00\x9fE50)l\x84\\\xc9\xcf', 111) - check_base58(b'A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\x1f\x8e\xa1p*{\xd4\x94\x1b\xca\tA\xb8R\xc4\xbb\xfe\xdb.\x05', 0) - check_base58(b':\x0b\x05\xf4\xd7\xf6l;\xa7\x00\x9fE50)l\x84\\\xc9\xcf', 0) - check_base58(b'A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) + check_base58(bytes.fromhex('1f8ea1702a7bd4941bca0941b852c4bbfedb2e05'), 111) + check_base58(bytes.fromhex('3a0b05f4d7f66c3ba7009f453530296c845cc9cf'), 111) + check_base58(bytes.fromhex('41c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('0041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('000041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('00000041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('1f8ea1702a7bd4941bca0941b852c4bbfedb2e05'), 0) + check_base58(bytes.fromhex('3a0b05f4d7f66c3ba7009f453530296c845cc9cf'), 0) + check_base58(bytes.fromhex('41c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('0041c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('000041c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('00000041c1eaf111802559bad61b60d62b1f897c63928a'), 0) diff --git a/test/functional/test_framework/bdb.py b/test/functional/test_framework/bdb.py new file mode 100644 index 0000000000..9de358aa0a --- /dev/null +++ b/test/functional/test_framework/bdb.py @@ -0,0 +1,152 @@ +#!/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. +""" +Utilities for working directly with the wallet's BDB database file + +This is specific to the configuration of BDB used in this project: + - pagesize: 4096 bytes + - Outer database contains single subdatabase named 'main' + - btree + - btree leaf pages + +Each key-value pair is two entries in a btree leaf. The first is the key, the one that follows +is the value. And so on. Note that the entry data is itself not in the correct order. Instead +entry offsets are stored in the correct order and those offsets are needed to then retrieve +the data itself. + +Page format can be found in BDB source code dbinc/db_page.h +This only implements the deserialization of btree metadata pages and normal btree pages. Overflow +pages are not implemented but may be needed in the future if dealing with wallets with large +transactions. + +`db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file +""" + +import binascii +import struct + +# Important constants +PAGESIZE = 4096 +OUTER_META_PAGE = 0 +INNER_META_PAGE = 2 + +# Page type values +BTREE_INTERNAL = 3 +BTREE_LEAF = 5 +BTREE_META = 9 + +# Some magic numbers for sanity checking +BTREE_MAGIC = 0x053162 +DB_VERSION = 9 + +# Deserializes a leaf page into a dict. +# Btree internal pages have the same header, for those, return None. +# For the btree leaf pages, deserialize them and put all the data into a dict +def dump_leaf_page(data): + page_info = {} + page_header = data[0:26] + _, pgno, prev_pgno, next_pgno, entries, hf_offset, level, pg_type = struct.unpack('QIIIHHBB', page_header) + page_info['pgno'] = pgno + page_info['prev_pgno'] = prev_pgno + page_info['next_pgno'] = next_pgno + page_info['entries'] = entries + page_info['hf_offset'] = hf_offset + page_info['level'] = level + page_info['pg_type'] = pg_type + page_info['entry_offsets'] = struct.unpack('{}H'.format(entries), data[26:26 + entries * 2]) + page_info['entries'] = [] + + if pg_type == BTREE_INTERNAL: + # Skip internal pages. These are the internal nodes of the btree and don't contain anything relevant to us + return None + + assert pg_type == BTREE_LEAF, 'A non-btree leaf page has been encountered while dumping leaves' + + for i in range(0, entries): + offset = page_info['entry_offsets'][i] + entry = {'offset': offset} + page_data_header = data[offset:offset + 3] + e_len, pg_type = struct.unpack('HB', page_data_header) + entry['len'] = e_len + entry['pg_type'] = pg_type + entry['data'] = data[offset + 3:offset + 3 + e_len] + page_info['entries'].append(entry) + + return page_info + +# Deserializes a btree metadata page into a dict. +# Does a simple sanity check on the magic value, type, and version +def dump_meta_page(page): + # metadata page + # general metadata + metadata = {} + meta_page = page[0:72] + _, pgno, magic, version, pagesize, encrypt_alg, pg_type, metaflags, _, free, last_pgno, nparts, key_count, record_count, flags, uid = struct.unpack('QIIIIBBBBIIIIII20s', meta_page) + metadata['pgno'] = pgno + metadata['magic'] = magic + metadata['version'] = version + metadata['pagesize'] = pagesize + metadata['encrypt_alg'] = encrypt_alg + metadata['pg_type'] = pg_type + metadata['metaflags'] = metaflags + metadata['free'] = free + metadata['last_pgno'] = last_pgno + metadata['nparts'] = nparts + metadata['key_count'] = key_count + metadata['record_count'] = record_count + metadata['flags'] = flags + metadata['uid'] = binascii.hexlify(uid) + + assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' + assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' + assert version == DB_VERSION, 'Database too new' + + # btree metadata + btree_meta_page = page[72:512] + _, minkey, re_len, re_pad, root, _, crypto_magic, _, iv, chksum = struct.unpack('IIIII368sI12s16s20s', btree_meta_page) + metadata['minkey'] = minkey + metadata['re_len'] = re_len + metadata['re_pad'] = re_pad + metadata['root'] = root + metadata['crypto_magic'] = crypto_magic + metadata['iv'] = binascii.hexlify(iv) + metadata['chksum'] = binascii.hexlify(chksum) + return metadata + +# Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict +def extract_kv_pairs(page_data): + out = {} + last_key = None + for i, entry in enumerate(page_data['entries']): + # By virtue of these all being pairs, even number entries are keys, and odd are values + if i % 2 == 0: + out[entry['data']] = b'' + last_key = entry['data'] + else: + out[last_key] = entry['data'] + return out + +# Extract the key-value pairs of the BDB file given in filename +def dump_bdb_kv(filename): + # Read in the BDB file and start deserializing it + pages = [] + with open(filename, 'rb') as f: + data = f.read(PAGESIZE) + while len(data) > 0: + pages.append(data) + data = f.read(PAGESIZE) + + # Sanity check the meta pages + dump_meta_page(pages[OUTER_META_PAGE]) + dump_meta_page(pages[INNER_META_PAGE]) + + # Fetch the kv pairs from the leaf pages + kv = {} + for i in range(3, len(pages)): + info = dump_leaf_page(pages[i]) + if info is not None: + info_kv = extract_kv_pairs(info) + kv = {**kv, **info_kv} + return kv diff --git a/test/functional/test_framework/bip340_test_vectors.csv b/test/functional/test_framework/bip340_test_vectors.csv new file mode 100644 index 0000000000..e068322deb --- /dev/null +++ b/test/functional/test_framework/bip340_test_vectors.csv @@ -0,0 +1,16 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index afc1995009..6b7214f03a 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -4,6 +4,9 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for manipulating blocks and transactions.""" +from binascii import a2b_hex +import struct +import time import unittest from .address import ( @@ -41,9 +44,10 @@ from .script import ( hash160, ) from .util import assert_equal -from io import BytesIO +WITNESS_SCALE_FACTOR = 4 MAX_BLOCK_SIGOPS = 20000 +MAX_BLOCK_SIGOPS_WEIGHT = MAX_BLOCK_SIGOPS * WITNESS_SCALE_FACTOR # Genesis block time (regtest) TIME_GENESIS_BLOCK = 1296688602 @@ -51,19 +55,29 @@ TIME_GENESIS_BLOCK = 1296688602 # From BIP141 WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed" +NORMAL_GBT_REQUEST_PARAMS = {"rules": ["segwit"]} -def create_block(hashprev, coinbase, ntime=None, *, version=1): + +def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl=None, txlist=None): """Create a block (with regtest difficulty).""" block = CBlock() - block.nVersion = version - if ntime is None: - import time - block.nTime = int(time.time() + 600) + if tmpl is None: + tmpl = {} + block.nVersion = version or tmpl.get('version') or 1 + block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600) + block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10) + if tmpl and not tmpl.get('bits') is None: + block.nBits = struct.unpack('>I', a2b_hex(tmpl['bits']))[0] else: - block.nTime = ntime - block.hashPrevBlock = hashprev - block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams + block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams + if coinbase is None: + coinbase = create_coinbase(height=tmpl['height']) block.vtx.append(coinbase) + if txlist: + for tx in txlist: + if not hasattr(tx, 'calc_sha256'): + tx = FromHex(CTransaction(), tx) + block.vtx.append(tx) block.hashMerkleRoot = block.calc_merkle_root() block.calc_sha256() return block @@ -101,22 +115,31 @@ def script_BIP34_coinbase_height(height): return CScript([CScriptNum(height)]) -def create_coinbase(height, pubkey=None): - """Create a coinbase transaction, assuming no miner fees. +def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0): + """Create a coinbase transaction. If pubkey is passed in, the coinbase output will be a P2PK output; - otherwise an anyone-can-spend output.""" + otherwise an anyone-can-spend output. + + If extra_output_script is given, make a 0-value output to that + script. This is useful to pad block weight/sigops as needed. """ coinbase = CTransaction() coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)) coinbaseoutput = CTxOut() coinbaseoutput.nValue = 50 * COIN halvings = int(height / 150) # regtest coinbaseoutput.nValue >>= halvings - if (pubkey is not None): + coinbaseoutput.nValue += fees + if pubkey is not None: coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG]) else: coinbaseoutput.scriptPubKey = CScript([OP_TRUE]) coinbase.vout = [coinbaseoutput] + if extra_output_script is not None: + coinbaseoutput2 = CTxOut() + coinbaseoutput2.nValue = 0 + coinbaseoutput2.scriptPubKey = extra_output_script + coinbase.vout.append(coinbaseoutput2) coinbase.calc_sha256() return coinbase @@ -135,25 +158,27 @@ def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, script_pub_key=C def create_transaction(node, txid, to_address, *, amount): """ Return signed transaction spending the first output of the - input txid. Note that the node must be able to sign for the - output that is being spent, and the node must not be running - multiple wallets. + input txid. Note that the node must have a wallet that can + sign for the output that is being spent. """ raw_tx = create_raw_transaction(node, txid, to_address, amount=amount) - tx = CTransaction() - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx))) + tx = FromHex(CTransaction(), raw_tx) return tx def create_raw_transaction(node, txid, to_address, *, amount): """ Return raw signed transaction spending the first output of the - input txid. Note that the node must be able to sign for the - output that is being spent, and the node must not be running - multiple wallets. + input txid. Note that the node must have a wallet that can sign + for the output that is being spent. """ - rawtx = node.createrawtransaction(inputs=[{"txid": txid, "vout": 0}], outputs={to_address: amount}) - signresult = node.signrawtransactionwithwallet(rawtx) - assert_equal(signresult["complete"], True) - return signresult['hex'] + psbt = node.createpsbt(inputs=[{"txid": txid, "vout": 0}], outputs={to_address: amount}) + for _ in range(2): + for w in node.listwallets(): + wrpc = node.get_wallet_rpc(w) + signed_psbt = wrpc.walletprocesspsbt(psbt) + psbt = signed_psbt['psbt'] + final_psbt = node.finalizepsbt(psbt) + assert_equal(final_psbt["complete"], True) + return final_psbt['hex'] def get_legacy_sigopcount_block(block, accurate=True): count = 0 diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index adbffb7dc7..f3d13c049b 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Pieter Wuille +# Copyright (c) 2019-2020 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test-only secp256k1 elliptic curve implementation @@ -6,10 +6,24 @@ WARNING: This code is slow, uses bad randomness, does not properly protect keys, and is trivially vulnerable to side channel attacks. Do not use for anything but tests.""" +import csv +import hashlib +import os import random +import unittest from .util import modinv +def TaggedHash(tag, data): + ss = hashlib.sha256(tag.encode('utf-8')).digest() + ss += ss + ss += data + return hashlib.sha256(ss).digest() + +def xor_bytes(b0, b1): + assert len(b0) == len(b1) + return bytes(x ^ y for (x, y) in zip(b0, b1)) + def jacobi_symbol(n, k): """Compute the Jacobi symbol of n modulo k @@ -68,6 +82,10 @@ class EllipticCurve: inv_3 = (inv_2 * inv) % self.p return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1) + def has_even_y(self, p1): + """Whether the point p1 has an even Y coordinate when expressed in affine coordinates.""" + return not (p1[2] == 0 or self.affine(p1)[1] & 1) + def negate(self, p1): """Negate a Jacobian point tuple p1.""" x1, y1, z1 = p1 @@ -86,13 +104,13 @@ class EllipticCurve: return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1 def lift_x(self, x): - """Given an X coordinate on the curve, return a corresponding affine point.""" + """Given an X coordinate on the curve, return a corresponding affine point for which the Y coordinate is even.""" x_3 = pow(x, 3, self.p) v = x_3 + self.a * x + self.b y = modsqrt(v, self.p) if y is None: return None - return (x, y, 1) + return (x, self.p - y if y & 1 else y, 1) def double(self, p1): """Double a Jacobian tuple p1 @@ -197,7 +215,8 @@ class EllipticCurve: r = self.add(r, p) return r -SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7) +SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977 +SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7) SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1) SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2 @@ -221,9 +240,9 @@ class ECPubKey(): x = int.from_bytes(data[1:33], 'big') if SECP256K1.is_x_coord(x): p = SECP256K1.lift_x(x) - # if the oddness of the y co-ord isn't correct, find the other - # valid y - if (p[1] & 1) != (data[0] & 1): + # Make the Y coordinate odd if required (lift_x always produces + # a point with an even Y coordinate). + if data[0] & 1: p = SECP256K1.negate(p) self.p = p self.valid = True @@ -303,10 +322,14 @@ class ECPubKey(): u1 = z*w % SECP256K1_ORDER u2 = r*w % SECP256K1_ORDER R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)])) - if R is None or R[0] != r: + if R is None or (R[0] % SECP256K1_ORDER) != r: return False return True +def generate_privkey(): + """Generate a valid random 32-byte private key.""" + return random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big') + class ECKey(): """A secp256k1 private key""" @@ -324,7 +347,7 @@ class ECKey(): def generate(self, compressed=True): """Generate a random private key (compressed or uncompressed).""" - self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed) + self.set(generate_privkey(), compressed) def get_bytes(self): """Retrieve the 32-byte representation of this key.""" @@ -369,3 +392,162 @@ class ECKey(): rb = r.to_bytes((r.bit_length() + 8) // 8, 'big') sb = s.to_bytes((s.bit_length() + 8) // 8, 'big') return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb + +def compute_xonly_pubkey(key): + """Compute an x-only (32 byte) public key from a (32 byte) private key. + + This also returns whether the resulting public key was negated. + """ + + assert len(key) == 32 + x = int.from_bytes(key, 'big') + if x == 0 or x >= SECP256K1_ORDER: + return (None, None) + P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, x)])) + return (P[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(P)) + +def tweak_add_privkey(key, tweak): + """Tweak a private key (after negating it if needed).""" + + assert len(key) == 32 + assert len(tweak) == 32 + + x = int.from_bytes(key, 'big') + if x == 0 or x >= SECP256K1_ORDER: + return None + if not SECP256K1.has_even_y(SECP256K1.mul([(SECP256K1_G, x)])): + x = SECP256K1_ORDER - x + t = int.from_bytes(tweak, 'big') + if t >= SECP256K1_ORDER: + return None + x = (x + t) % SECP256K1_ORDER + if x == 0: + return None + return x.to_bytes(32, 'big') + +def tweak_add_pubkey(key, tweak): + """Tweak a public key and return whether the result had to be negated.""" + + assert len(key) == 32 + assert len(tweak) == 32 + + x_coord = int.from_bytes(key, 'big') + if x_coord >= SECP256K1_FIELD_SIZE: + return None + P = SECP256K1.lift_x(x_coord) + if P is None: + return None + t = int.from_bytes(tweak, 'big') + if t >= SECP256K1_ORDER: + return None + Q = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, t), (P, 1)])) + if Q is None: + return None + return (Q[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(Q)) + +def verify_schnorr(key, sig, msg): + """Verify a Schnorr signature (see BIP 340). + + - key is a 32-byte xonly pubkey (computed using compute_xonly_pubkey). + - sig is a 64-byte Schnorr signature + - msg is a 32-byte message + """ + assert len(key) == 32 + assert len(msg) == 32 + assert len(sig) == 64 + + x_coord = int.from_bytes(key, 'big') + if x_coord == 0 or x_coord >= SECP256K1_FIELD_SIZE: + return False + P = SECP256K1.lift_x(x_coord) + if P is None: + return False + r = int.from_bytes(sig[0:32], 'big') + if r >= SECP256K1_FIELD_SIZE: + return False + s = int.from_bytes(sig[32:64], 'big') + if s >= SECP256K1_ORDER: + return False + e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), 'big') % SECP256K1_ORDER + R = SECP256K1.mul([(SECP256K1_G, s), (P, SECP256K1_ORDER - e)]) + if not SECP256K1.has_even_y(R): + return False + if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]: + return False + return True + +def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): + """Create a Schnorr signature (see BIP 340).""" + + if aux is None: + aux = bytes(32) + + assert len(key) == 32 + assert len(msg) == 32 + assert len(aux) == 32 + + sec = int.from_bytes(key, 'big') + if sec == 0 or sec >= SECP256K1_ORDER: + return None + P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, sec)])) + if SECP256K1.has_even_y(P) == flip_p: + sec = SECP256K1_ORDER - sec + t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big') + kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER + assert kp != 0 + R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) + k = kp if SECP256K1.has_even_y(R) != flip_r else SECP256K1_ORDER - kp + e = int.from_bytes(TaggedHash("BIP0340/challenge", R[0].to_bytes(32, 'big') + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER + return R[0].to_bytes(32, 'big') + ((k + e * sec) % SECP256K1_ORDER).to_bytes(32, 'big') + +class TestFrameworkKey(unittest.TestCase): + def test_schnorr(self): + """Test the Python Schnorr implementation.""" + byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, SECP256K1_ORDER - 1, SECP256K1_ORDER, 2**256 - 1]] + keys = {} + for privkey in byte_arrays: # build array of key/pubkey pairs + pubkey, _ = compute_xonly_pubkey(privkey) + if pubkey is not None: + keys[privkey] = pubkey + for msg in byte_arrays: # test every combination of message, signing key, verification key + for sign_privkey, sign_pubkey in keys.items(): + sig = sign_schnorr(sign_privkey, msg) + for verify_privkey, verify_pubkey in keys.items(): + if verify_privkey == sign_privkey: + self.assertTrue(verify_schnorr(verify_pubkey, sig, msg)) + sig = list(sig) + sig[random.randrange(64)] ^= (1 << (random.randrange(8))) # damaging signature should break things + sig = bytes(sig) + self.assertFalse(verify_schnorr(verify_pubkey, sig, msg)) + + def test_schnorr_testvectors(self): + """Implement the BIP340 test vectors (read from bip340_test_vectors.csv).""" + num_tests = 0 + vectors_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bip340_test_vectors.csv') + with open(vectors_file, newline='', encoding='utf8') as csvfile: + reader = csv.reader(csvfile) + next(reader) + for row in reader: + (i_str, seckey_hex, pubkey_hex, aux_rand_hex, msg_hex, sig_hex, result_str, comment) = row + i = int(i_str) + pubkey = bytes.fromhex(pubkey_hex) + msg = bytes.fromhex(msg_hex) + sig = bytes.fromhex(sig_hex) + result = result_str == 'TRUE' + if seckey_hex != '': + seckey = bytes.fromhex(seckey_hex) + pubkey_actual = compute_xonly_pubkey(seckey)[0] + self.assertEqual(pubkey.hex(), pubkey_actual.hex(), "BIP340 test vector %i (%s): pubkey mismatch" % (i, comment)) + aux_rand = bytes.fromhex(aux_rand_hex) + try: + sig_actual = sign_schnorr(seckey, msg, aux_rand) + self.assertEqual(sig.hex(), sig_actual.hex(), "BIP340 test vector %i (%s): sig mismatch" % (i, comment)) + except RuntimeError as e: + self.fail("BIP340 test vector %i (%s): signing raised exception %s" % (i, comment, e)) + result_actual = verify_schnorr(pubkey, sig, msg) + if result: + self.assertEqual(result, result_actual, "BIP340 test vector %i (%s): verification failed" % (i, comment)) + else: + self.assertEqual(result, result_actual, "BIP340 test vector %i (%s): verification succeeded unexpectedly" % (i, comment)) + num_tests += 1 + self.assertTrue(num_tests >= 15) # expect at least 15 test vectors diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 00cf1ef66d..bab4ad0008 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -51,7 +51,6 @@ MAX_HEADERS_RESULTS = 2000 # Number of headers sent in one getheaders result MAX_INV_SIZE = 50000 # Maximum number of entries in an 'inv' protocol message NODE_NETWORK = (1 << 0) -NODE_GETUTXO = (1 << 1) NODE_BLOOM = (1 << 2) NODE_WITNESS = (1 << 3) NODE_COMPACT_FILTERS = (1 << 6) @@ -136,12 +135,17 @@ def uint256_from_compact(c): return v -def deser_vector(f, c): +# deser_function_name: Allow for an alternate deserialization function on the +# entries in the vector. +def deser_vector(f, c, deser_function_name=None): nit = deser_compact_size(f) r = [] for _ in range(nit): t = c() - t.deserialize(f) + if deser_function_name: + getattr(t, deser_function_name)(f) + else: + t.deserialize(f) r.append(t) return r @@ -204,38 +208,82 @@ def ToHex(obj): class CAddress: - __slots__ = ("ip", "nServices", "pchReserved", "port", "time") + __slots__ = ("net", "ip", "nServices", "port", "time") + + # see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki + NET_IPV4 = 1 + + ADDRV2_NET_NAME = { + NET_IPV4: "IPv4" + } + + ADDRV2_ADDRESS_LENGTH = { + NET_IPV4: 4 + } def __init__(self): self.time = 0 self.nServices = 1 - self.pchReserved = b"\x00" * 10 + b"\xff" * 2 + self.net = self.NET_IPV4 self.ip = "0.0.0.0" self.port = 0 def deserialize(self, f, *, with_time=True): + """Deserialize from addrv1 format (pre-BIP155)""" if with_time: # VERSION messages serialize CAddress objects without time - self.time = struct.unpack("<i", f.read(4))[0] + self.time = struct.unpack("<I", f.read(4))[0] self.nServices = struct.unpack("<Q", f.read(8))[0] - self.pchReserved = f.read(12) + # We only support IPv4 which means skip 12 bytes and read the next 4 as IPv4 address. + f.read(12) + self.net = self.NET_IPV4 self.ip = socket.inet_ntoa(f.read(4)) self.port = struct.unpack(">H", f.read(2))[0] def serialize(self, *, with_time=True): + """Serialize in addrv1 format (pre-BIP155)""" + assert self.net == self.NET_IPV4 r = b"" if with_time: # VERSION messages serialize CAddress objects without time - r += struct.pack("<i", self.time) + r += struct.pack("<I", self.time) r += struct.pack("<Q", self.nServices) - r += self.pchReserved + r += b"\x00" * 10 + b"\xff" * 2 + r += socket.inet_aton(self.ip) + r += struct.pack(">H", self.port) + return r + + def deserialize_v2(self, f): + """Deserialize from addrv2 format (BIP155)""" + self.time = struct.unpack("<I", f.read(4))[0] + + self.nServices = deser_compact_size(f) + + self.net = struct.unpack("B", f.read(1))[0] + assert self.net == self.NET_IPV4 + + address_length = deser_compact_size(f) + assert address_length == self.ADDRV2_ADDRESS_LENGTH[self.net] + + self.ip = socket.inet_ntoa(f.read(4)) + + self.port = struct.unpack(">H", f.read(2))[0] + + def serialize_v2(self): + """Serialize in addrv2 format (BIP155)""" + assert self.net == self.NET_IPV4 + r = b"" + r += struct.pack("<I", self.time) + r += ser_compact_size(self.nServices) + r += struct.pack("B", self.net) + r += ser_compact_size(self.ADDRV2_ADDRESS_LENGTH[self.net]) r += socket.inet_aton(self.ip) r += struct.pack(">H", self.port) return r def __repr__(self): - return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, - self.ip, self.port) + return ("CAddress(nServices=%i net=%s addr=%s port=%i)" + % (self.nServices, self.ADDRV2_NET_NAME[self.net], self.ip, self.port)) class CInv: @@ -1064,6 +1112,40 @@ class msg_addr: return "msg_addr(addrs=%s)" % (repr(self.addrs)) +class msg_addrv2: + __slots__ = ("addrs",) + msgtype = b"addrv2" + + def __init__(self): + self.addrs = [] + + def deserialize(self, f): + self.addrs = deser_vector(f, CAddress, "deserialize_v2") + + def serialize(self): + return ser_vector(self.addrs, "serialize_v2") + + def __repr__(self): + return "msg_addrv2(addrs=%s)" % (repr(self.addrs)) + + +class msg_sendaddrv2: + __slots__ = () + msgtype = b"sendaddrv2" + + def __init__(self): + pass + + def deserialize(self, f): + pass + + def serialize(self): + return b"" + + def __repr__(self): + return "msg_sendaddrv2()" + + class msg_inv: __slots__ = ("inv",) msgtype = b"inv" diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index 5f9b316b18..8b79a4dc2f 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -33,6 +33,7 @@ from test_framework.messages import ( MAX_HEADERS_RESULTS, MIN_VERSION_SUPPORTED, msg_addr, + msg_addrv2, msg_block, MSG_BLOCK, msg_blocktxn, @@ -56,6 +57,7 @@ from test_framework.messages import ( msg_notfound, msg_ping, msg_pong, + msg_sendaddrv2, msg_sendcmpct, msg_sendheaders, msg_tx, @@ -75,6 +77,7 @@ logger = logging.getLogger("TestFramework.p2p") MESSAGEMAP = { b"addr": msg_addr, + b"addrv2": msg_addrv2, b"block": msg_block, b"blocktxn": msg_blocktxn, b"cfcheckpt": msg_cfcheckpt, @@ -97,6 +100,7 @@ MESSAGEMAP = { b"notfound": msg_notfound, b"ping": msg_ping, b"pong": msg_pong, + b"sendaddrv2": msg_sendaddrv2, b"sendcmpct": msg_sendcmpct, b"sendheaders": msg_sendheaders, b"tx": msg_tx, @@ -285,7 +289,7 @@ class P2PInterface(P2PConnection): Individual testcases should subclass this and override the on_* methods if they want to alter message handling behaviour.""" - def __init__(self): + def __init__(self, support_addrv2=False): super().__init__() # Track number of messages of each type received. @@ -303,6 +307,8 @@ class P2PInterface(P2PConnection): # The network services received from the peer self.nServices = 0 + self.support_addrv2 = support_addrv2 + def peer_connect(self, *args, services=NODE_NETWORK|NODE_WITNESS, send_version=True, **kwargs): create_conn = super().peer_connect(*args, **kwargs) @@ -345,6 +351,7 @@ class P2PInterface(P2PConnection): pass def on_addr(self, message): pass + def on_addrv2(self, message): pass def on_block(self, message): pass def on_blocktxn(self, message): pass def on_cfcheckpt(self, message): pass @@ -365,6 +372,7 @@ class P2PInterface(P2PConnection): def on_merkleblock(self, message): pass def on_notfound(self, message): pass def on_pong(self, message): pass + def on_sendaddrv2(self, message): pass def on_sendcmpct(self, message): pass def on_sendheaders(self, message): pass def on_tx(self, message): pass @@ -388,6 +396,8 @@ class P2PInterface(P2PConnection): assert message.nVersion >= MIN_VERSION_SUPPORTED, "Version {} received. Test framework only supports versions greater than {}".format(message.nVersion, MIN_VERSION_SUPPORTED) if message.nVersion >= 70016: self.send_message(msg_wtxidrelay()) + if self.support_addrv2: + self.send_message(msg_sendaddrv2()) self.send_message(msg_verack()) self.nServices = message.nServices diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 5e35ba0fce..26ccab3039 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -6,11 +6,15 @@ This file is modified from python-bitcoinlib. """ + +from collections import namedtuple import hashlib import struct import unittest from typing import List, Dict +from .key import TaggedHash, tweak_add_pubkey + from .messages import ( CTransaction, CTxOut, @@ -22,8 +26,13 @@ from .messages import ( ) MAX_SCRIPT_ELEMENT_SIZE = 520 +LOCKTIME_THRESHOLD = 500000000 +ANNEX_TAG = 0x50 + OPCODE_NAMES = {} # type: Dict[CScriptOp, str] +LEAF_VERSION_TAPSCRIPT = 0xc0 + def hash160(s): return hashlib.new('ripemd160', sha256(s)).digest() @@ -239,11 +248,8 @@ OP_NOP8 = CScriptOp(0xb7) OP_NOP9 = CScriptOp(0xb8) OP_NOP10 = CScriptOp(0xb9) -# template matching params -OP_SMALLINTEGER = CScriptOp(0xfa) -OP_PUBKEYS = CScriptOp(0xfb) -OP_PUBKEYHASH = CScriptOp(0xfd) -OP_PUBKEY = CScriptOp(0xfe) +# BIP 342 opcodes (Tapscript) +OP_CHECKSIGADD = CScriptOp(0xba) OP_INVALIDOPCODE = CScriptOp(0xff) @@ -359,10 +365,7 @@ OPCODE_NAMES.update({ OP_NOP8: 'OP_NOP8', OP_NOP9: 'OP_NOP9', OP_NOP10: 'OP_NOP10', - OP_SMALLINTEGER: 'OP_SMALLINTEGER', - OP_PUBKEYS: 'OP_PUBKEYS', - OP_PUBKEYHASH: 'OP_PUBKEYHASH', - OP_PUBKEY: 'OP_PUBKEY', + OP_CHECKSIGADD: 'OP_CHECKSIGADD', OP_INVALIDOPCODE: 'OP_INVALIDOPCODE', }) @@ -593,6 +596,7 @@ class CScript(bytes): return n +SIGHASH_DEFAULT = 0 # Taproot-only default, semantics same as SIGHASH_ALL SIGHASH_ALL = 1 SIGHASH_NONE = 2 SIGHASH_SINGLE = 3 @@ -615,7 +619,6 @@ def FindAndDelete(script, sig): r += script[last_sop_idx:] return CScript(r) - def LegacySignatureHash(script, txTo, inIdx, hashtype): """Consensus-correct SignatureHash @@ -738,3 +741,125 @@ class TestFrameworkScript(unittest.TestCase): values = [0, 1, -1, -2, 127, 128, -255, 256, (1 << 15) - 1, -(1 << 16), (1 << 24) - 1, (1 << 31), 1 - (1 << 32), 1 << 40, 1500, -1500] for value in values: self.assertEqual(CScriptNum.decode(CScriptNum.encode(CScriptNum(value))), value) + +def TaprootSignatureHash(txTo, spent_utxos, hash_type, input_index = 0, scriptpath = False, script = CScript(), codeseparator_pos = -1, annex = None, leaf_ver = LEAF_VERSION_TAPSCRIPT): + assert (len(txTo.vin) == len(spent_utxos)) + assert (input_index < len(txTo.vin)) + out_type = SIGHASH_ALL if hash_type == 0 else hash_type & 3 + in_type = hash_type & SIGHASH_ANYONECANPAY + spk = spent_utxos[input_index].scriptPubKey + ss = bytes([0, hash_type]) # epoch, hash_type + ss += struct.pack("<i", txTo.nVersion) + ss += struct.pack("<I", txTo.nLockTime) + if in_type != SIGHASH_ANYONECANPAY: + ss += sha256(b"".join(i.prevout.serialize() for i in txTo.vin)) + ss += sha256(b"".join(struct.pack("<q", u.nValue) for u in spent_utxos)) + ss += sha256(b"".join(ser_string(u.scriptPubKey) for u in spent_utxos)) + ss += sha256(b"".join(struct.pack("<I", i.nSequence) for i in txTo.vin)) + if out_type == SIGHASH_ALL: + ss += sha256(b"".join(o.serialize() for o in txTo.vout)) + spend_type = 0 + if annex is not None: + spend_type |= 1 + if (scriptpath): + spend_type |= 2 + ss += bytes([spend_type]) + if in_type == SIGHASH_ANYONECANPAY: + ss += txTo.vin[input_index].prevout.serialize() + ss += struct.pack("<q", spent_utxos[input_index].nValue) + ss += ser_string(spk) + ss += struct.pack("<I", txTo.vin[input_index].nSequence) + else: + ss += struct.pack("<I", input_index) + if (spend_type & 1): + ss += sha256(ser_string(annex)) + if out_type == SIGHASH_SINGLE: + if input_index < len(txTo.vout): + ss += sha256(txTo.vout[input_index].serialize()) + else: + ss += bytes(0 for _ in range(32)) + if (scriptpath): + ss += TaggedHash("TapLeaf", bytes([leaf_ver]) + ser_string(script)) + ss += bytes([0]) + ss += struct.pack("<i", codeseparator_pos) + assert len(ss) == 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37 + return TaggedHash("TapSighash", ss) + +def taproot_tree_helper(scripts): + if len(scripts) == 0: + return ([], bytes(0 for _ in range(32))) + if len(scripts) == 1: + # One entry: treat as a leaf + script = scripts[0] + assert(not callable(script)) + if isinstance(script, list): + return taproot_tree_helper(script) + assert(isinstance(script, tuple)) + version = LEAF_VERSION_TAPSCRIPT + name = script[0] + code = script[1] + if len(script) == 3: + version = script[2] + assert version & 1 == 0 + assert isinstance(code, bytes) + h = TaggedHash("TapLeaf", bytes([version]) + ser_string(code)) + if name is None: + return ([], h) + return ([(name, version, code, bytes())], h) + elif len(scripts) == 2 and callable(scripts[1]): + # Two entries, and the right one is a function + left, left_h = taproot_tree_helper(scripts[0:1]) + right_h = scripts[1](left_h) + left = [(name, version, script, control + right_h) for name, version, script, control in left] + right = [] + else: + # Two or more entries: descend into each side + split_pos = len(scripts) // 2 + left, left_h = taproot_tree_helper(scripts[0:split_pos]) + right, right_h = taproot_tree_helper(scripts[split_pos:]) + left = [(name, version, script, control + right_h) for name, version, script, control in left] + right = [(name, version, script, control + left_h) for name, version, script, control in right] + if right_h < left_h: + right_h, left_h = left_h, right_h + h = TaggedHash("TapBranch", left_h + right_h) + return (left + right, h) + +# A TaprootInfo object has the following fields: +# - scriptPubKey: the scriptPubKey (witness v1 CScript) +# - inner_pubkey: the inner pubkey (32 bytes) +# - negflag: whether the pubkey in the scriptPubKey was negated from inner_pubkey+tweak*G (bool). +# - tweak: the tweak (32 bytes) +# - leaves: a dict of name -> TaprootLeafInfo objects for all known leaves +TaprootInfo = namedtuple("TaprootInfo", "scriptPubKey,inner_pubkey,negflag,tweak,leaves") + +# A TaprootLeafInfo object has the following fields: +# - script: the leaf script (CScript or bytes) +# - version: the leaf version (0xc0 for BIP342 tapscript) +# - merklebranch: the merkle branch to use for this leaf (32*N bytes) +TaprootLeafInfo = namedtuple("TaprootLeafInfo", "script,version,merklebranch") + +def taproot_construct(pubkey, scripts=None): + """Construct a tree of Taproot spending conditions + + pubkey: a 32-byte xonly pubkey for the internal pubkey (bytes) + scripts: a list of items; each item is either: + - a (name, CScript or bytes, leaf version) tuple + - a (name, CScript or bytes) tuple (defaulting to leaf version 0xc0) + - another list of items (with the same structure) + - a list of two items; the first of which is an item itself, and the + second is a function. The function takes as input the Merkle root of the + first item, and produces a (fictitious) partner to hash with. + + Returns: a TaprootInfo object + """ + if scripts is None: + scripts = [] + + ret, h = taproot_tree_helper(scripts) + tweak = TaggedHash("TapTweak", pubkey + h) + tweaked, negated = tweak_add_pubkey(pubkey, tweak) + leaves = dict((name, TaprootLeafInfo(script, version, merklebranch)) for name, version, script, merklebranch in ret) + return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves) + +def is_op_success(o): + return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 80fbae70bf..318a438705 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -3,7 +3,8 @@ # 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 +from test_framework.script import CScript, hash160, sha256, OP_0, OP_DUP, OP_HASH160, OP_CHECKSIG, OP_EQUAL, OP_EQUALVERIFY +from test_framework.util import hex_str_to_bytes # 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 @@ -24,3 +25,59 @@ from test_framework.script import CScript # met. DUMMY_P2WPKH_SCRIPT = CScript([b'a' * 21]) DUMMY_2_P2WPKH_SCRIPT = CScript([b'b' * 21]) + +def keyhash_to_p2pkh_script(hash, main = False): + assert len(hash) == 20 + return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) + +def scripthash_to_p2sh_script(hash, main = False): + assert len(hash) == 20 + return CScript([OP_HASH160, hash, OP_EQUAL]) + +def key_to_p2pkh_script(key, main = False): + key = check_key(key) + return keyhash_to_p2pkh_script(hash160(key), main) + +def script_to_p2sh_script(script, main = False): + script = check_script(script) + return scripthash_to_p2sh_script(hash160(script), main) + +def key_to_p2sh_p2wpkh_script(key, main = False): + key = check_key(key) + p2shscript = CScript([OP_0, hash160(key)]) + return script_to_p2sh_script(p2shscript, main) + +def program_to_witness_script(version, program, main = False): + if isinstance(program, str): + program = hex_str_to_bytes(program) + assert 0 <= version <= 16 + assert 2 <= len(program) <= 40 + assert version > 0 or len(program) in [20, 32] + return CScript([version, program]) + +def script_to_p2wsh_script(script, main = False): + script = check_script(script) + return program_to_witness_script(0, sha256(script), main) + +def key_to_p2wpkh_script(key, main = False): + key = check_key(key) + return program_to_witness_script(0, hash160(key), main) + +def script_to_p2sh_p2wsh_script(script, main = False): + script = check_script(script) + p2shscript = CScript([OP_0, sha256(script)]) + return script_to_p2sh_script(p2shscript, main) + +def check_key(key): + if isinstance(key, str): + key = hex_str_to_bytes(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 = hex_str_to_bytes(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/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 02368e938f..00c0d8a919 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Reference implementation for Bech32 and segwit addresses.""" - +import unittest CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -84,7 +84,7 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode(hrp, addr): +def decode_segwit_address(hrp, addr): """Decode a segwit address.""" hrpgot, data = bech32_decode(addr) if hrpgot != hrp: @@ -99,9 +99,23 @@ def decode(hrp, addr): return (data[0], decoded) -def encode(hrp, witver, witprog): +def encode_segwit_address(hrp, witver, witprog): """Encode a segwit address.""" ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) - if decode(hrp, ret) == (None, None): + if decode_segwit_address(hrp, ret) == (None, None): return None return ret + +class TestFrameworkScript(unittest.TestCase): + def test_segwit_encode_decode(self): + def test_python_bech32(addr): + hrp = addr[:4] + self.assertEqual(hrp, "bcrt") + (witver, witprog) = decode_segwit_address(hrp, addr) + self.assertEqual(encode_segwit_address(hrp, witver, witprog), addr) + + # P2WPKH + test_python_bech32('bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46') + # P2WSH + test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') + test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index f41f5129b8..bf047c5f68 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -27,10 +27,9 @@ from .util import ( PortSeed, assert_equal, check_json_precision, - connect_nodes, - disconnect_nodes, get_datadir_path, initialize_datadir, + p2p_port, wait_until_helper, ) @@ -102,8 +101,17 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond self.supports_cli = True self.bind_to_localhost_only = True - self.set_test_params() self.parse_args() + 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 + # create and import keys to. If unset, default is len(nodes) * + # [default_wallet_name]. If wallet names are None, wallet creation is + # skipped. If list is truncated, wallet creation is skipped and keys + # are not imported. + self.wallet_names = None + self.set_test_params() + assert self.wallet_names is None or len(self.wallet_names) <= self.num_nodes if self.options.timeout_factor == 0 : self.options.timeout_factor = 99999 self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) # optionally, increase timeout by a factor @@ -175,9 +183,14 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") - parser.add_argument("--descriptors", default=False, action="store_true", - help="Run test using a descriptor wallet") parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts') + + group = parser.add_mutually_exclusive_group() + group.add_argument("--descriptors", default=False, action="store_true", + help="Run test using a descriptor wallet", dest='descriptors') + group.add_argument("--legacy-wallet", default=False, action="store_false", + help="Run test using legacy wallets", dest='descriptors') + self.add_options(parser) self.options = parser.parse_args() self.options.previous_releases_path = previous_releases_path @@ -362,23 +375,12 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def setup_nodes(self): """Override this method to customize test node setup""" extra_args = [[]] * self.num_nodes - wallets = [[]] * self.num_nodes if hasattr(self, "extra_args"): extra_args = self.extra_args - wallets = [[x for x in eargs if x.startswith('-wallet=')] for eargs in extra_args] - extra_args = [x + ['-nowallet'] for x in extra_args] self.add_nodes(self.num_nodes, extra_args) self.start_nodes() - for i, n in enumerate(self.nodes): - n.extra_args.pop() - if '-wallet=0' in n.extra_args or '-nowallet' in n.extra_args or '-disablewallet' in n.extra_args or not self.is_wallet_compiled(): - continue - if '-wallet=' not in wallets[i] and not any([x.startswith('-wallet=') for x in wallets[i]]): - wallets[i].append('-wallet=') - for w in wallets[i]: - wallet_name = w.split('=', 1)[1] - n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors) - self.import_deterministic_coinbase_privkeys() + if self.is_wallet_compiled(): + self.import_deterministic_coinbase_privkeys() if not self.setup_clean_chain: for n in self.nodes: assert_equal(n.getblockchaininfo()["blocks"], 199) @@ -394,13 +396,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): assert_equal(chain_info["initialblockdownload"], False) def import_deterministic_coinbase_privkeys(self): - for n in self.nodes: - try: - n.getwalletinfo() - except JSONRPCException as e: - assert str(e).startswith('Method not found') - continue - + for i in range(self.num_nodes): + self.init_wallet(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 + if wallet_name is not False: + n = self.nodes[i] + 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') def run_test(self): @@ -534,10 +538,49 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.nodes[i].process.wait(timeout) def connect_nodes(self, a, b): - connect_nodes(self.nodes[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) def disconnect_nodes(self, a, b): - disconnect_nodes(self.nodes[a], b) + def disconnect_nodes_helper(from_connection, node_num): + def get_peer_ids(): + result = [] + for peer in from_connection.getpeerinfo(): + if "testnode{}".format(node_num) in peer['subver']: + result.append(peer['id']) + return result + + peer_ids = get_peer_ids() + if not peer_ids: + self.log.warning("disconnect_nodes: {} and {} were not connected".format( + from_connection.index, + node_num, + )) + return + for peer_id in peer_ids: + try: + from_connection.disconnectnode(nodeid=peer_id) + except JSONRPCException as e: + # If this node is disconnected between calculating the peer id + # and issuing the disconnect, don't worry about it. + # This avoids a race condition if we're mass-disconnecting peers. + if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED + raise + + # wait to disconnect + wait_until_helper(lambda: not get_peer_ids(), timeout=5) + + disconnect_nodes_helper(self.nodes[a], b) def split_network(self): """ @@ -731,6 +774,18 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): """Skip the running test if wallet has not been compiled.""" if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") + if self.options.descriptors: + self.skip_if_no_sqlite() + + def skip_if_no_sqlite(self): + """Skip the running test if sqlite has not been compiled.""" + if not self.is_sqlite_compiled(): + raise SkipTest("sqlite has not been compiled.") + + def skip_if_no_bdb(self): + """Skip the running test if BDB has not been compiled.""" + if not self.is_bdb_compiled(): + raise SkipTest("BDB has not been compiled.") def skip_if_no_wallet_tool(self): """Skip the running test if bitcoin-wallet has not been compiled.""" @@ -770,3 +825,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def is_zmq_compiled(self): """Checks whether the zmq module was compiled.""" return self.config["components"].getboolean("ENABLE_ZMQ") + + def is_sqlite_compiled(self): + """Checks whether the wallet module was compiled with Sqlite support.""" + return self.config["components"].getboolean("USE_SQLITE") + + def is_bdb_compiled(self): + """Checks whether the wallet module was compiled with BDB support.""" + return self.config["components"].getboolean("USE_BDB") diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 046efe730e..0a5b7f551c 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -544,7 +544,7 @@ class TestNode(): def num_test_p2p_connections(self): """Return number of test framework p2p connections to the node.""" - return len([peer for peer in self.getpeerinfo() if peer['subver'] == MY_SUBVERSION]) + return len([peer for peer in self.getpeerinfo() if peer['subver'] == MY_SUBVERSION.decode("utf-8")]) def disconnect_p2ps(self): """Close all p2p connections to the node.""" diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index af7f0b62f4..62ff5c6e33 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -8,11 +8,11 @@ from base64 import b64encode from binascii import unhexlify from decimal import Decimal, ROUND_DOWN from subprocess import CalledProcessError +import hashlib import inspect import json import logging import os -import random import re import time import unittest @@ -261,6 +261,14 @@ def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'), raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) raise RuntimeError('Unreachable') +def sha256sum_file(filename): + h = hashlib.sha256() + with open(filename, 'rb') as f: + d = f.read(4096) + while len(d) > 0: + h.update(d) + d = f.read(4096) + return h.digest() # RPC/P2P connection constants and functions ############################################ @@ -412,47 +420,6 @@ def set_node_times(nodes, t): node.setmocktime(t) -def disconnect_nodes(from_connection, node_num): - def get_peer_ids(): - result = [] - for peer in from_connection.getpeerinfo(): - if "testnode{}".format(node_num) in peer['subver']: - result.append(peer['id']) - return result - - peer_ids = get_peer_ids() - if not peer_ids: - logger.warning("disconnect_nodes: {} and {} were not connected".format( - from_connection.index, - node_num, - )) - return - for peer_id in peer_ids: - try: - from_connection.disconnectnode(nodeid=peer_id) - except JSONRPCException as e: - # If this node is disconnected between calculating the peer id - # and issuing the disconnect, don't worry about it. - # This avoids a race condition if we're mass-disconnecting peers. - if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED - raise - - # wait to disconnect - wait_until_helper(lambda: not get_peer_ids(), timeout=5) - - -def connect_nodes(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())) - - # Transaction/Block functions ############################# @@ -469,62 +436,6 @@ def find_output(node, txid, amount, *, blockhash=None): raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount))) -def gather_inputs(from_node, amount_needed, confirmations_required=1): - """ - Return a random set of unspent txouts that are enough to pay amount_needed - """ - assert confirmations_required >= 0 - utxo = from_node.listunspent(confirmations_required) - random.shuffle(utxo) - inputs = [] - total_in = Decimal("0.00000000") - while total_in < amount_needed and len(utxo) > 0: - t = utxo.pop() - total_in += t["amount"] - inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]}) - if total_in < amount_needed: - raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in)) - return (total_in, inputs) - - -def make_change(from_node, amount_in, amount_out, fee): - """ - Create change output(s), return them - """ - outputs = {} - amount = amount_out + fee - change = amount_in - amount - if change > amount * 2: - # Create an extra change output to break up big inputs - change_address = from_node.getnewaddress() - # Split change in two, being careful of rounding: - outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) - change = amount_in - amount - outputs[change_address] - if change > 0: - outputs[from_node.getnewaddress()] = change - return outputs - - -def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): - """ - Create a random transaction. - Returns (txid, hex-encoded-transaction-data, fee) - """ - from_node = random.choice(nodes) - to_node = random.choice(nodes) - fee = min_fee + fee_increment * random.randint(0, fee_variants) - - (total_in, inputs) = gather_inputs(from_node, amount + fee) - outputs = make_change(from_node, total_in, amount, fee) - outputs[to_node.getnewaddress()] = float(amount) - - rawtx = from_node.createrawtransaction(inputs, outputs) - signresult = from_node.signrawtransactionwithwallet(rawtx) - txid = from_node.sendrawtransaction(signresult["hex"], 0) - - return (txid, signresult["hex"], fee) - - # Helper to create at least "count" utxos # Pass in a fee that is sufficient for relay and mining new transactions. def create_confirmed_utxos(fee, node, count): diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 39b3bf2a5b..a71f2c69cb 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -40,9 +40,20 @@ class MiniWallet: self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value']}) return blocks - def get_utxo(self): - """Return the last utxo. Can be used to get the change output immediately after a send_self_transfer""" - return self._utxos.pop() + def get_utxo(self, *, txid=''): + """ + Returns a utxo and marks it as spent (pops it from the internal list) + + Args: + txid (string), optional: get the first utxo we find from a specific transaction + + Note: Can be used to get the change output immediately after a send_self_transfer + """ + index = -1 # by default the last utxo + if txid: + utxo = next(filter(lambda utxo: txid == utxo['txid'], self._utxos)) + index = self._utxos.index(utxo) + return self._utxos.pop(index) def send_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None): """Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c8cf173d5f..5b3db282e1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -70,7 +70,9 @@ TEST_FRAMEWORK_MODULES = [ "address", "blocktools", "muhash", + "key", "script", + "segwit_addr", "util", ] @@ -87,13 +89,15 @@ BASE_SCRIPTS = [ 'wallet_hd.py', 'wallet_hd.py --descriptors', 'wallet_backup.py', + 'wallet_backup.py --descriptors', # vv Tests less than 5m vv 'mining_getblocktemplate_longpoll.py', 'feature_maxuploadtarget.py', 'feature_block.py', 'rpc_fundrawtransaction.py', + 'rpc_fundrawtransaction.py --descriptors', 'p2p_compactblocks.py', - 'feature_segwit.py', + 'feature_segwit.py --legacy-wallet', # vv Tests less than 2m vv 'wallet_basic.py', 'wallet_basic.py --descriptors', @@ -103,18 +107,24 @@ BASE_SCRIPTS = [ 'p2p_timeouts.py', 'p2p_tx_download.py', 'mempool_updatefromblock.py', - 'wallet_dump.py', + 'wallet_dump.py --legacy-wallet', 'wallet_listtransactions.py', + 'wallet_listtransactions.py --descriptors', + 'feature_taproot.py', # vv Tests less than 60s vv 'p2p_sendheaders.py', - 'wallet_importmulti.py', + 'wallet_importmulti.py --legacy-wallet', 'mempool_limit.py', 'rpc_txoutproof.py', 'wallet_listreceivedby.py', + 'wallet_listreceivedby.py --descriptors', 'wallet_abandonconflict.py', + 'wallet_abandonconflict.py --descriptors', 'feature_csv_activation.py', 'rpc_rawtransaction.py', + 'rpc_rawtransaction.py --descriptors', 'wallet_address_types.py', + 'wallet_address_types.py --descriptors', 'feature_bip68_sequence.py', 'p2p_feefilter.py', 'feature_reindex.py', @@ -128,6 +138,7 @@ BASE_SCRIPTS = [ 'mempool_resurrect.py', 'wallet_txn_doublespend.py --mineblock', 'tool_wallet.py', + 'tool_wallet.py --descriptors', 'wallet_txn_clone.py', 'wallet_txn_clone.py --segwit', 'rpc_getchaintips.py', @@ -139,11 +150,13 @@ BASE_SCRIPTS = [ 'mempool_reorg.py', 'mempool_persist.py', 'wallet_multiwallet.py', + 'wallet_multiwallet.py --descriptors', 'wallet_multiwallet.py --usecli', 'wallet_createwallet.py', 'wallet_createwallet.py --usecli', - 'wallet_watchonly.py', - 'wallet_watchonly.py --usecli', + 'wallet_createwallet.py --descriptors', + 'wallet_watchonly.py --legacy-wallet', + 'wallet_watchonly.py --usecli --legacy-wallet', 'wallet_reorgsrestore.py', 'interface_http.py', 'interface_rpc.py', @@ -153,19 +166,23 @@ BASE_SCRIPTS = [ 'rpc_whitelist.py', 'feature_proxy.py', 'rpc_signrawtransaction.py', + 'rpc_signrawtransaction.py --descriptors', 'wallet_groups.py', + 'p2p_addrv2_relay.py', + 'wallet_groups.py --descriptors', 'p2p_disconnect_ban.py', 'rpc_decodescript.py', 'rpc_blockchain.py', 'rpc_deprecated.py', 'wallet_disable.py', + 'wallet_disable.py --descriptors', 'p2p_addr_relay.py', 'p2p_getaddr_caching.py', 'p2p_getdata.py', 'rpc_net.py', 'wallet_keypool.py', 'wallet_keypool.py --descriptors', - 'wallet_descriptor.py', + 'wallet_descriptor.py --descriptors', 'p2p_nobloomfilter_messages.py', 'p2p_filter.py', 'rpc_setban.py', @@ -178,7 +195,9 @@ BASE_SCRIPTS = [ 'feature_assumevalid.py', 'example_test.py', 'wallet_txn_doublespend.py', + 'wallet_txn_doublespend.py --descriptors', 'feature_backwards_compatibility.py', + 'feature_backwards_compatibility.py --descriptors', 'wallet_txn_clone.py --mineblock', 'feature_notifications.py', 'rpc_getblockfilter.py', @@ -191,18 +210,21 @@ BASE_SCRIPTS = [ 'feature_versionbits_warning.py', 'rpc_preciousblock.py', 'wallet_importprunedfunds.py', + 'wallet_importprunedfunds.py --descriptors', 'p2p_leak_tx.py', 'p2p_eviction.py', 'rpc_signmessage.py', 'rpc_generateblock.py', 'rpc_generate.py', 'wallet_balance.py', + 'wallet_balance.py --descriptors', 'feature_nulldummy.py', + 'feature_nulldummy.py --descriptors', 'mempool_accept.py', 'mempool_expiry.py', - 'wallet_import_rescan.py', - 'wallet_import_with_label.py', - 'wallet_importdescriptors.py', + 'wallet_import_rescan.py --legacy-wallet', + 'wallet_import_with_label.py --legacy-wallet', + 'wallet_importdescriptors.py --descriptors', 'wallet_upgradewallet.py', 'rpc_bind.py --ipv4', 'rpc_bind.py --ipv6', @@ -210,9 +232,11 @@ BASE_SCRIPTS = [ 'mining_basic.py', 'feature_signet.py', 'wallet_bumpfee.py', - 'wallet_implicitsegwit.py', + 'wallet_bumpfee.py --descriptors', + 'wallet_implicitsegwit.py --legacy-wallet', 'rpc_named_arguments.py', 'wallet_listsinceblock.py', + 'wallet_listsinceblock.py --descriptors', 'p2p_leak.py', 'wallet_encryption.py', 'wallet_encryption.py --descriptors', @@ -220,16 +244,20 @@ BASE_SCRIPTS = [ 'feature_cltv.py', 'rpc_uptime.py', 'wallet_resendwallettransactions.py', + 'wallet_resendwallettransactions.py --descriptors', 'wallet_fallbackfee.py', + 'wallet_fallbackfee.py --descriptors', 'rpc_dumptxoutset.py', 'feature_minchainwork.py', 'rpc_estimatefee.py', 'rpc_getblockstats.py', 'wallet_create_tx.py', 'wallet_send.py', + 'wallet_create_tx.py --descriptors', 'p2p_fingerprint.py', 'feature_uacomment.py', 'wallet_coinbase_category.py', + 'wallet_coinbase_category.py --descriptors', 'feature_filelock.py', 'feature_loadblock.py', 'p2p_dos_header_tree.py', diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index fa5b5c10ff..615b772dc8 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -71,9 +71,12 @@ class ToolWalletTest(BitcoinTestFramework): self.assert_raises_tool_error('Error: two methods provided (info and create). Only one method should be provided.', 'info', 'create') self.assert_raises_tool_error('Error parsing command line arguments: Invalid parameter -foo', '-foo') 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?" self.assert_raises_tool_error( - 'Error initializing wallet database environment "{}"!'.format(locked_dir), - '-wallet=wallet.dat', + error, + '-wallet=' + self.default_wallet_name, 'info', ) path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "nonexistent.dat") @@ -95,16 +98,34 @@ class ToolWalletTest(BitcoinTestFramework): # shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - out = textwrap.dedent('''\ - Wallet info - =========== - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 0 - Address Book: 3 - ''') - self.assert_tool_output(out, '-wallet=wallet.dat', 'info') + if self.options.descriptors: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: default_wallet + Format: sqlite + Descriptors: yes + Encrypted: no + HD (hd seed available): yes + Keypool Size: 6 + Transactions: 0 + Address Book: 1 + ''') + else: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: \ + + Format: bdb + Descriptors: no + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 0 + Address Book: 3 + ''') + self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') timestamp_after = self.wallet_timestamp() self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after)) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) @@ -134,16 +155,34 @@ class ToolWalletTest(BitcoinTestFramework): shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - out = textwrap.dedent('''\ - Wallet info - =========== - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 1 - Address Book: 3 - ''') - self.assert_tool_output(out, '-wallet=wallet.dat', 'info') + if self.options.descriptors: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: default_wallet + Format: sqlite + Descriptors: yes + Encrypted: no + HD (hd seed available): yes + Keypool Size: 6 + Transactions: 1 + Address Book: 1 + ''') + else: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: \ + + Format: bdb + Descriptors: no + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 1 + Address Book: 3 + ''') + self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after)) @@ -164,6 +203,9 @@ class ToolWalletTest(BitcoinTestFramework): Topping up keypool... Wallet info =========== + Name: foo + Format: bdb + Descriptors: no Encrypted: no HD (hd seed available): yes Keypool Size: 2000 @@ -181,7 +223,7 @@ class ToolWalletTest(BitcoinTestFramework): def test_getwalletinfo_on_different_wallet(self): self.log.info('Starting node with arg -wallet=foo') - self.start_node(0, ['-wallet=foo']) + self.start_node(0, ['-nowallet', '-wallet=foo']) self.log.info('Calling getwalletinfo on a different wallet ("foo"), testing output') shasum_before = self.wallet_shasum() @@ -207,20 +249,24 @@ class ToolWalletTest(BitcoinTestFramework): def test_salvage(self): # TODO: Check salvage actually salvages and doesn't break things. https://github.com/bitcoin/bitcoin/issues/7463 self.log.info('Check salvage') - self.start_node(0, ['-wallet=salvage']) + self.start_node(0) + self.nodes[0].createwallet("salvage") self.stop_node(0) self.assert_tool_output('', '-wallet=salvage', 'salvage') def run_test(self): - self.wallet_path = os.path.join(self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat') + self.wallet_path = os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename) self.test_invalid_tool_commands_and_args() # Warning: The following tests are order-dependent. self.test_tool_wallet_info() self.test_tool_wallet_info_after_transaction() - self.test_tool_wallet_create_on_existing_wallet() - self.test_getwalletinfo_on_different_wallet() - self.test_salvage() + if not self.options.descriptors: + # TODO: Wallet tool needs more create options at which point these can be enabled. + self.test_tool_wallet_create_on_existing_wallet() + self.test_getwalletinfo_on_different_wallet() + # Salvage is a legacy wallet only thing + self.test_salvage() if __name__ == '__main__': ToolWalletTest().main() diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index 8837e13005..2e0edcfa38 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -16,8 +16,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, - disconnect_nodes, ) @@ -50,7 +48,7 @@ class AbandonConflictTest(BitcoinTestFramework): balance = newbalance # Disconnect nodes so node0's transactions don't get into node1's mempool - disconnect_nodes(self.nodes[0], 1) + self.disconnect_nodes(0, 1) # Identify the 10btc outputs nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10")) @@ -161,7 +159,7 @@ class AbandonConflictTest(BitcoinTestFramework): self.nodes[1].sendrawtransaction(signed["hex"]) self.nodes[1].generate(1) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks() # Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index 68e22b7e86..2db5eae33b 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -62,11 +62,6 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - connect_nodes, -) -from test_framework.segwit_addr import ( - encode, - decode, ) class AddressTypeTest(BitcoinTestFramework): @@ -94,20 +89,13 @@ class AddressTypeTest(BitcoinTestFramework): # Fully mesh-connect nodes for faster mempool sync for i, j in itertools.product(range(self.num_nodes), repeat=2): if i > j: - connect_nodes(self.nodes[i], j) + self.connect_nodes(i, j) self.sync_all() def get_balances(self, key='trusted'): """Return a list of balances.""" return [self.nodes[i].getbalances()['mine'][key] for i in range(4)] - # Quick test of python bech32 implementation - def test_python_bech32(self, addr): - hrp = addr[:4] - assert_equal(hrp, "bcrt") - (witver, witprog) = decode(hrp, addr) - assert_equal(encode(hrp, witver, witprog), addr) - def test_address(self, node, address, multisig, typ): """Run sanity checks on an address.""" info = self.nodes[node].getaddressinfo(address) @@ -132,7 +120,6 @@ class AddressTypeTest(BitcoinTestFramework): assert_equal(info['witness_version'], 0) assert_equal(len(info['witness_program']), 40) assert 'pubkey' in info - self.test_python_bech32(info["address"]) elif typ == 'legacy': # P2SH-multisig assert info['isscript'] @@ -158,7 +145,6 @@ class AddressTypeTest(BitcoinTestFramework): assert_equal(info['witness_version'], 0) assert_equal(len(info['witness_program']), 64) assert 'pubkeys' in info - self.test_python_bech32(info["address"]) else: # Unknown type assert False @@ -242,18 +228,25 @@ class AddressTypeTest(BitcoinTestFramework): compressed_1 = "0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52" compressed_2 = "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073" - # addmultisigaddress with at least 1 uncompressed key should return a legacy address. - for node in range(4): - self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, uncompressed_2])['address'], True, 'legacy') - self.test_address(node, self.nodes[node].addmultisigaddress(2, [compressed_1, uncompressed_2])['address'], True, 'legacy') - self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, compressed_2])['address'], True, 'legacy') - # addmultisigaddress with all compressed keys should return the appropriate address type (even when the keys are not ours). - self.test_address(0, self.nodes[0].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'legacy') - self.test_address(1, self.nodes[1].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') - self.test_address(2, self.nodes[2].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') - self.test_address(3, self.nodes[3].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'bech32') - - for explicit_type, multisig, from_node in itertools.product([False, True], [False, True], range(4)): + if not self.options.descriptors: + # Tests for addmultisigaddress's address type behavior is only for legacy wallets. + # Descriptor wallets do not have addmultsigaddress so these tests are not needed for those. + # addmultisigaddress with at least 1 uncompressed key should return a legacy address. + for node in range(4): + self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, uncompressed_2])['address'], True, 'legacy') + self.test_address(node, self.nodes[node].addmultisigaddress(2, [compressed_1, uncompressed_2])['address'], True, 'legacy') + self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, compressed_2])['address'], True, 'legacy') + # addmultisigaddress with all compressed keys should return the appropriate address type (even when the keys are not ours). + self.test_address(0, self.nodes[0].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'legacy') + self.test_address(1, self.nodes[1].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') + self.test_address(2, self.nodes[2].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') + self.test_address(3, self.nodes[3].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'bech32') + + do_multisigs = [False] + if not self.options.descriptors: + do_multisigs.append(True) + + for explicit_type, multisig, from_node in itertools.product([False, True], do_multisigs, range(4)): address_type = None if explicit_type and not multisig: if from_node == 1: diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index eddd938847..229c134a4b 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -9,7 +9,6 @@ from test_framework.util import ( assert_approx, assert_equal, assert_raises_rpc_error, - connect_nodes, ) def reset_balance(node, discardaddr): @@ -111,7 +110,7 @@ class AvoidReuseTest(BitcoinTestFramework): assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) self.restart_node(1) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) # Flags should still be node1.avoid_reuse=false, node2.avoid_reuse=true assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index bcbac18d57..f34c1345e0 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -39,7 +39,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, ) @@ -50,10 +49,10 @@ class WalletBackupTest(BitcoinTestFramework): # nodes 1, 2,3 are spenders, let's give them a keypool=100 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [ - ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], - ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], - ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], - ["-whitelist=noban@127.0.0.1", "-wallet="], + ["-whitelist=noban@127.0.0.1", "-keypool=100"], + ["-whitelist=noban@127.0.0.1", "-keypool=100"], + ["-whitelist=noban@127.0.0.1", "-keypool=100"], + ["-whitelist=noban@127.0.0.1"], ] self.rpc_timeout = 120 @@ -62,10 +61,10 @@ class WalletBackupTest(BitcoinTestFramework): def setup_network(self): self.setup_nodes() - connect_nodes(self.nodes[0], 3) - connect_nodes(self.nodes[1], 3) - connect_nodes(self.nodes[2], 3) - connect_nodes(self.nodes[2], 0) + self.connect_nodes(0, 3) + self.connect_nodes(1, 3) + self.connect_nodes(2, 3) + self.connect_nodes(2, 0) self.sync_all() def one_send(self, from_node, to_address): @@ -92,14 +91,14 @@ class WalletBackupTest(BitcoinTestFramework): self.sync_blocks() # As above, this mirrors the original bash test. - def start_three(self): - self.start_node(0) - self.start_node(1) - self.start_node(2) - connect_nodes(self.nodes[0], 3) - connect_nodes(self.nodes[1], 3) - connect_nodes(self.nodes[2], 3) - connect_nodes(self.nodes[2], 0) + def start_three(self, args=()): + self.start_node(0, self.extra_args[0] + list(args)) + self.start_node(1, self.extra_args[1] + list(args)) + self.start_node(2, self.extra_args[2] + list(args)) + self.connect_nodes(0, 3) + self.connect_nodes(1, 3) + self.connect_nodes(2, 3) + self.connect_nodes(2, 0) def stop_three(self): self.stop_node(0) @@ -107,9 +106,14 @@ class WalletBackupTest(BitcoinTestFramework): self.stop_node(2) def erase_three(self): - os.remove(os.path.join(self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat')) - os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', 'wallet.dat')) - os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', 'wallet.dat')) + os.remove(os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + + def init_three(self): + self.init_wallet(0) + self.init_wallet(1) + self.init_wallet(2) def run_test(self): self.log.info("Generating initial blockchain") @@ -135,11 +139,13 @@ class WalletBackupTest(BitcoinTestFramework): self.log.info("Backing up") self.nodes[0].backupwallet(os.path.join(self.nodes[0].datadir, 'wallet.bak')) - self.nodes[0].dumpwallet(os.path.join(self.nodes[0].datadir, 'wallet.dump')) self.nodes[1].backupwallet(os.path.join(self.nodes[1].datadir, 'wallet.bak')) - self.nodes[1].dumpwallet(os.path.join(self.nodes[1].datadir, 'wallet.dump')) self.nodes[2].backupwallet(os.path.join(self.nodes[2].datadir, 'wallet.bak')) - self.nodes[2].dumpwallet(os.path.join(self.nodes[2].datadir, 'wallet.dump')) + + if not self.options.descriptors: + self.nodes[0].dumpwallet(os.path.join(self.nodes[0].datadir, 'wallet.dump')) + self.nodes[1].dumpwallet(os.path.join(self.nodes[1].datadir, 'wallet.dump')) + self.nodes[2].dumpwallet(os.path.join(self.nodes[2].datadir, 'wallet.dump')) self.log.info("More transactions") for _ in range(5): @@ -171,9 +177,9 @@ class WalletBackupTest(BitcoinTestFramework): shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) # Restore wallets from backup - shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat')) - shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', 'wallet.dat')) - shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join(self.nodes[2].datadir, self.chain, 'wallets', 'wallet.dat')) + shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) self.log.info("Re-starting nodes") self.start_three() @@ -183,35 +189,37 @@ class WalletBackupTest(BitcoinTestFramework): assert_equal(self.nodes[1].getbalance(), balance1) assert_equal(self.nodes[2].getbalance(), balance2) - self.log.info("Restoring using dumped wallet") - self.stop_three() - self.erase_three() + if not self.options.descriptors: + self.log.info("Restoring using dumped wallet") + self.stop_three() + self.erase_three() - #start node2 with no chain - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks')) - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) + #start node2 with no chain + shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks')) + shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) - self.start_three() + self.start_three(["-nowallet"]) + self.init_three() - assert_equal(self.nodes[0].getbalance(), 0) - assert_equal(self.nodes[1].getbalance(), 0) - assert_equal(self.nodes[2].getbalance(), 0) + assert_equal(self.nodes[0].getbalance(), 0) + assert_equal(self.nodes[1].getbalance(), 0) + assert_equal(self.nodes[2].getbalance(), 0) - self.nodes[0].importwallet(os.path.join(self.nodes[0].datadir, 'wallet.dump')) - self.nodes[1].importwallet(os.path.join(self.nodes[1].datadir, 'wallet.dump')) - self.nodes[2].importwallet(os.path.join(self.nodes[2].datadir, 'wallet.dump')) + self.nodes[0].importwallet(os.path.join(self.nodes[0].datadir, 'wallet.dump')) + self.nodes[1].importwallet(os.path.join(self.nodes[1].datadir, 'wallet.dump')) + self.nodes[2].importwallet(os.path.join(self.nodes[2].datadir, 'wallet.dump')) - self.sync_blocks() + self.sync_blocks() - assert_equal(self.nodes[0].getbalance(), balance0) - assert_equal(self.nodes[1].getbalance(), balance1) - assert_equal(self.nodes[2].getbalance(), balance2) + assert_equal(self.nodes[0].getbalance(), balance0) + assert_equal(self.nodes[1].getbalance(), balance1) + assert_equal(self.nodes[2].getbalance(), balance2) # Backup to source wallet file must fail sourcePaths = [ - os.path.join(self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat'), - os.path.join(self.nodes[0].datadir, self.chain, '.', 'wallets', 'wallet.dat'), - os.path.join(self.nodes[0].datadir, self.chain, 'wallets', ''), + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename), + os.path.join(self.nodes[0].datadir, self.chain, '.', 'wallets', self.default_wallet_name, self.wallet_data_filename), + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name), os.path.join(self.nodes[0].datadir, self.chain, 'wallets')] for sourcePath in sourcePaths: diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 31829a18b3..433b40faee 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -11,7 +11,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - connect_nodes, ) @@ -58,14 +57,16 @@ class WalletTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - self.nodes[0].importaddress(ADDRESS_WATCHONLY) - # Check that nodes don't own any UTXOs - assert_equal(len(self.nodes[0].listunspent()), 0) - assert_equal(len(self.nodes[1].listunspent()), 0) + if not self.options.descriptors: + # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets + self.nodes[0].importaddress(ADDRESS_WATCHONLY) + # Check that nodes don't own any UTXOs + assert_equal(len(self.nodes[0].listunspent()), 0) + assert_equal(len(self.nodes[1].listunspent()), 0) - self.log.info("Check that only node 0 is watching an address") - assert 'watchonly' in self.nodes[0].getbalances() - assert 'watchonly' not in self.nodes[1].getbalances() + self.log.info("Check that only node 0 is watching an address") + assert 'watchonly' in self.nodes[0].getbalances() + assert 'watchonly' not in self.nodes[1].getbalances() self.log.info("Mining blocks ...") self.nodes[0].generate(1) @@ -74,22 +75,28 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].generatetoaddress(101, ADDRESS_WATCHONLY) self.sync_all() - assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50) - assert_equal(self.nodes[0].getwalletinfo()['balance'], 50) - assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50) + if not self.options.descriptors: + # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets + assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50) + assert_equal(self.nodes[0].getwalletinfo()['balance'], 50) + assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50) - assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000) - assert 'watchonly' not in self.nodes[1].getbalances() + assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000) + assert 'watchonly' not in self.nodes[1].getbalances() - assert_equal(self.nodes[0].getbalance(), 50) - assert_equal(self.nodes[1].getbalance(), 50) + assert_equal(self.nodes[0].getbalance(), 50) + assert_equal(self.nodes[1].getbalance(), 50) self.log.info("Test getbalance with different arguments") assert_equal(self.nodes[0].getbalance("*"), 50) assert_equal(self.nodes[0].getbalance("*", 1), 50) - assert_equal(self.nodes[0].getbalance("*", 1, True), 100) assert_equal(self.nodes[0].getbalance(minconf=1), 50) - assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 100) + if not self.options.descriptors: + assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 100) + assert_equal(self.nodes[0].getbalance("*", 1, True), 100) + else: + assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 50) + assert_equal(self.nodes[0].getbalance("*", 1, True), 50) assert_equal(self.nodes[1].getbalance(minconf=0, include_watchonly=True), 50) # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. @@ -157,6 +164,8 @@ class WalletTest(BitcoinTestFramework): expected_balances_1 = {'mine': {'immature': Decimal('0E-8'), 'trusted': Decimal('0E-8'), # node 1's send had an unsafe input 'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent + if self.options.descriptors: + del expected_balances_0["watchonly"] assert_equal(self.nodes[0].getbalances(), expected_balances_0) assert_equal(self.nodes[1].getbalances(), expected_balances_1) # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions @@ -215,10 +224,10 @@ class WalletTest(BitcoinTestFramework): # dynamically loading the wallet. before = self.nodes[1].getbalances()['mine']['untrusted_pending'] dst = self.nodes[1].getnewaddress() - self.nodes[1].unloadwallet('') + self.nodes[1].unloadwallet(self.default_wallet_name) self.nodes[0].sendtoaddress(dst, 0.1) self.sync_all() - self.nodes[1].loadwallet('') + self.nodes[1].loadwallet(self.default_wallet_name) after = self.nodes[1].getbalances()['mine']['untrusted_pending'] assert_equal(before + Decimal('0.1'), after) @@ -254,15 +263,13 @@ class WalletTest(BitcoinTestFramework): self.log.info('Put txs back into mempool of node 1 (not node 0)') self.nodes[0].invalidateblock(block_reorg) self.nodes[1].invalidateblock(block_reorg) - self.sync_blocks() - self.nodes[0].syncwithvalidationinterfacequeue() assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted self.nodes[0].generatetoaddress(1, ADDRESS_WATCHONLY) assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted # Now confirm tx_orig self.restart_node(1, ['-persistmempool=0']) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_blocks() self.nodes[1].sendrawtransaction(tx_orig) self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY) diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index bb208341a0..ac4a6e4948 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet.""" from decimal import Decimal +from itertools import product from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -11,10 +12,11 @@ from test_framework.util import ( assert_equal, assert_fee_amount, assert_raises_rpc_error, - connect_nodes, ) from test_framework.wallet_util import test_address +OUT_OF_RANGE = "Amount out of range" + class WalletTest(BitcoinTestFramework): def set_test_params(self): @@ -32,9 +34,9 @@ class WalletTest(BitcoinTestFramework): self.setup_nodes() # Only need nodes 0-2 running at start of test self.stop_node(3) - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) self.sync_all(self.nodes[0:3]) def check_fee_amount(self, curr_balance, balance_with_fee, fee_per_byte, tx_size): @@ -75,7 +77,7 @@ class WalletTest(BitcoinTestFramework): assert_equal(len(self.nodes[1].listunspent()), 1) assert_equal(len(self.nodes[2].listunspent()), 0) - self.log.info("test gettxout") + self.log.info("Test gettxout") confirmed_txid, confirmed_index = utxos[0]["txid"], utxos[0]["vout"] # First, outputs that are unspent both in the chain and in the # mempool should appear with or without include_mempool @@ -88,7 +90,7 @@ class WalletTest(BitcoinTestFramework): self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 11) mempool_txid = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 10) - self.log.info("test gettxout (second part)") + self.log.info("Test gettxout (second part)") # utxo spent in mempool should be visible if you exclude mempool # but invisible if you include mempool txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False) @@ -210,6 +212,8 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) + self.log.info("Test sendmany") + # Sendmany 10 BTC txid = self.nodes[2].sendmany('', {address: 10}, 0, "", []) self.nodes[2].generate(1) @@ -226,62 +230,43 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) - # Sendmany with explicit fee (BTC/kB) - # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", - self.nodes[2].sendmany, - amounts={ address: 10 }, - estimate_mode='bTc/kB') - # Throw if negative feerate - assert_raises_rpc_error(-3, "Amount out of range", - self.nodes[2].sendmany, - amounts={ address: 10 }, - conf_target=-1, - estimate_mode='bTc/kB') - fee_per_kb = 0.0002500 - explicit_fee_per_byte = Decimal(fee_per_kb) / 1000 - txid = self.nodes[2].sendmany( - amounts={ address: 10 }, - conf_target=fee_per_kb, - estimate_mode='bTc/kB', - ) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) - node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), node_2_bal - Decimal('10'), explicit_fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) - assert_equal(self.nodes[2].getbalance(), node_2_bal) - node_0_bal += Decimal('10') - assert_equal(self.nodes[0].getbalance(), node_0_bal) + self.log.info("Test sendmany with fee_rate param (explicit fee rate in sat/vB)") + fee_rate_sat_vb = 2 + fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8 + explicit_fee_rate_btc_kvb = Decimal(fee_rate_btc_kvb) / 1000 - # Sendmany with explicit fee (SAT/B) - # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", - self.nodes[2].sendmany, - amounts={ address: 10 }, - estimate_mode='sat/b') - # Throw if negative feerate - assert_raises_rpc_error(-3, "Amount out of range", - self.nodes[2].sendmany, - amounts={ address: 10 }, - conf_target=-1, - estimate_mode='sat/b') - fee_sat_per_b = 2 - fee_per_kb = fee_sat_per_b / 100000.0 - explicit_fee_per_byte = Decimal(fee_per_kb) / 1000 - txid = self.nodes[2].sendmany( - amounts={ address: 10 }, - conf_target=fee_sat_per_b, - estimate_mode='sAT/b', - ) + txid = self.nodes[2].sendmany(amounts={address: 10}, fee_rate=fee_rate_sat_vb) self.nodes[2].generate(1) self.sync_all(self.nodes[0:3]) balance = self.nodes[2].getbalance() - node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) + node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_rate_btc_kvb, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) assert_equal(balance, node_2_bal) node_0_bal += Decimal('10') assert_equal(self.nodes[0].getbalance(), node_0_bal) + for key in ["totalFee", "feeRate"]: + assert_raises_rpc_error(-8, "Unknown named parameter key", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=1, key=1) + + # Test setting explicit fee rate just below the minimum. + self.log.info("Test sendmany raises 'fee rate too low' if fee_rate of 0.99999999 is passed") + assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", + self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.99999999) + + self.log.info("Test sendmany raises if fee_rate of 0 or -1 is passed") + assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", + self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0) + assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=-1) + + self.log.info("Test sendmany raises if an invalid conf_target or estimate_mode is passed") + for target, mode in product([-1, 0, 1009], ["economical", "conservative"]): + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h + self.nodes[2].sendmany, amounts={address: 1}, conf_target=target, estimate_mode=mode) + for target, mode in product([-1, 0], ["btc/kb", "sat/b"]): + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', + self.nodes[2].sendmany, amounts={address: 1}, conf_target=target, estimate_mode=mode) + self.start_node(3, self.nodes[3].extra_args) - connect_nodes(self.nodes[0], 3) + self.connect_nodes(0, 3) self.sync_all() # check if we can list zero value tx as available coins @@ -311,14 +296,14 @@ class WalletTest(BitcoinTestFramework): assert_equal(uTx['amount'], Decimal('0')) assert found - # do some -walletbroadcast tests + self.log.info("Test -walletbroadcast") self.stop_nodes() self.start_node(0, ["-walletbroadcast=0"]) self.start_node(1, ["-walletbroadcast=0"]) self.start_node(2, ["-walletbroadcast=0"]) - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) self.sync_all(self.nodes[0:3]) txid_not_broadcast = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 2) @@ -343,9 +328,9 @@ class WalletTest(BitcoinTestFramework): self.start_node(0) self.start_node(1) self.start_node(2) - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) self.sync_blocks(self.nodes[0:3]) self.nodes[0].generate(1) @@ -371,7 +356,7 @@ class WalletTest(BitcoinTestFramework): # General checks for errors from incorrect inputs # This will raise an exception because the amount is negative - assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "-1") + assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "-1") # This will raise an exception because the amount type is wrong assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "1f-4") @@ -413,73 +398,42 @@ class WalletTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) - # send with explicit btc/kb fee - self.log.info("test explicit fee (sendtoaddress as btc/kb)") - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) + self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in sat/vB)") prebalance = self.nodes[2].getbalance() assert prebalance > 2 address = self.nodes[1].getnewaddress() - # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", - self.nodes[2].sendtoaddress, - address=address, - amount=1.0, - estimate_mode='BTc/Kb') - # Throw if negative feerate - assert_raises_rpc_error(-3, "Amount out of range", - self.nodes[2].sendtoaddress, - address=address, - amount=1.0, - conf_target=-1, - estimate_mode='btc/kb') - txid = self.nodes[2].sendtoaddress( - address=address, - amount=1.0, - conf_target=0.00002500, - estimate_mode='btc/kb', - ) - tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex']) - self.sync_all(self.nodes[0:3]) - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) - postbalance = self.nodes[2].getbalance() - fee = prebalance - postbalance - Decimal('1') - assert_fee_amount(fee, tx_size, Decimal('0.00002500')) + amount = 3 + fee_rate_sat_vb = 2 + fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8 - # send with explicit sat/b fee - self.sync_all(self.nodes[0:3]) - self.log.info("test explicit fee (sendtoaddress as sat/b)") - self.nodes[0].generate(1) - prebalance = self.nodes[2].getbalance() - assert prebalance > 2 - address = self.nodes[1].getnewaddress() - # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", - self.nodes[2].sendtoaddress, - address=address, - amount=1.0, - estimate_mode='SAT/b') - # Throw if negative feerate - assert_raises_rpc_error(-3, "Amount out of range", - self.nodes[2].sendtoaddress, - address=address, - amount=1.0, - conf_target=-1, - estimate_mode='SAT/b') - txid = self.nodes[2].sendtoaddress( - address=address, - amount=1.0, - conf_target=2, - estimate_mode='SAT/B', - ) + txid = self.nodes[2].sendtoaddress(address=address, amount=amount, fee_rate=fee_rate_sat_vb) tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex']) - self.sync_all(self.nodes[0:3]) self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) postbalance = self.nodes[2].getbalance() - fee = prebalance - postbalance - Decimal('1') - assert_fee_amount(fee, tx_size, Decimal('0.00002000')) + fee = prebalance - postbalance - Decimal(amount) + assert_fee_amount(fee, tx_size, Decimal(fee_rate_btc_kvb)) + + for key in ["totalFee", "feeRate"]: + assert_raises_rpc_error(-8, "Unknown named parameter key", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=1, key=1) + + # Test setting explicit fee rate just below the minimum. + self.log.info("Test sendtoaddress raises 'fee rate too low' if fee_rate of 0.99999999 is passed") + assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", + self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.99999999) + + self.log.info("Test sendtoaddress raises if fee_rate of 0 or -1 is passed") + assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", + self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=0) + assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=-1) + + self.log.info("Test sendtoaddress raises if an invalid conf_target or estimate_mode is passed") + for target, mode in product([-1, 0, 1009], ["economical", "conservative"]): + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h + self.nodes[2].sendtoaddress, address=address, amount=1, conf_target=target, estimate_mode=mode) + for target, mode in product([-1, 0], ["btc/kb", "sat/b"]): + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', + self.nodes[2].sendtoaddress, address=address, amount=1, conf_target=target, estimate_mode=mode) # 2. Import address from node2 to node1 self.nodes[1].importaddress(address_to_import) @@ -537,7 +491,7 @@ class WalletTest(BitcoinTestFramework): ] chainlimit = 6 for m in maintenance: - self.log.info("check " + m) + self.log.info("Test " + m) self.stop_nodes() # set lower ancestor limit for later self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)]) @@ -661,6 +615,18 @@ class WalletTest(BitcoinTestFramework): assert_array_result(tx["details"], {"category": "receive"}, expected_receive_vout) assert_equal(tx[verbose_field], self.nodes[0].decoderawtransaction(tx["hex"])) + self.log.info("Test send* RPCs with verbose=True") + address = self.nodes[0].getnewaddress("test") + txid_feeReason_one = self.nodes[2].sendtoaddress(address=address, amount=5, verbose=True) + assert_equal(txid_feeReason_one["fee_reason"], "Fallback fee") + txid_feeReason_two = self.nodes[2].sendmany(dummy='', amounts={address: 5}, verbose=True) + assert_equal(txid_feeReason_two["fee_reason"], "Fallback fee") + self.log.info("Test send* RPCs with verbose=False") + txid_feeReason_three = self.nodes[2].sendtoaddress(address=address, amount=5, verbose=False) + assert_equal(self.nodes[2].gettransaction(txid_feeReason_three)['txid'], txid_feeReason_three) + txid_feeReason_four = self.nodes[2].sendmany(dummy='', amounts={address: 5}, verbose=False) + assert_equal(self.nodes[2].gettransaction(txid_feeReason_four)['txid'], txid_feeReason_four) + if __name__ == '__main__': WalletTest().main() diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 56d1da60b7..99c9737258 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -29,12 +29,12 @@ from test_framework.util import ( WALLET_PASSPHRASE = "test" WALLET_PASSPHRASE_TIMEOUT = 3600 -# Fee rates (in BTC per 1000 vbytes) -INSUFFICIENT = 0.00001000 -ECONOMICAL = 0.00050000 -NORMAL = 0.00100000 -HIGH = 0.00500000 -TOO_HIGH = 1.00000000 +# Fee rates (sat/vB) +INSUFFICIENT = 1 +ECONOMICAL = 50 +NORMAL = 100 +HIGH = 500 +TOO_HIGH = 100000 class BumpFeeTest(BitcoinTestFramework): @@ -76,10 +76,9 @@ class BumpFeeTest(BitcoinTestFramework): self.log.info("Running tests") dest_address = peer_node.getnewaddress() - self.test_invalid_parameters(rbf_node, dest_address) - test_simple_bumpfee_succeeds(self, "default", rbf_node, peer_node, dest_address) - test_simple_bumpfee_succeeds(self, "fee_rate", rbf_node, peer_node, dest_address) - test_feerate_args(self, rbf_node, peer_node, dest_address) + for mode in ["default", "fee_rate"]: + test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address) + self.test_invalid_parameters(rbf_node, peer_node, dest_address) test_segwit_bumpfee_succeeds(self, rbf_node, dest_address) test_nonrbf_bumpfee_fails(self, peer_node, dest_address) test_notmine_bumpfee_fails(self, rbf_node, peer_node, dest_address) @@ -98,28 +97,48 @@ class BumpFeeTest(BitcoinTestFramework): test_small_output_with_feerate_succeeds(self, rbf_node, dest_address) test_no_more_inputs_fails(self, rbf_node, dest_address) - def test_invalid_parameters(self, node, dest_address): - txid = spend_one_input(node, dest_address) - # invalid estimate mode - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": "moo", - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": 38, - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": { - "foo": "bar", - }, - }) - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": Decimal("3.141592"), - }) - # confTarget and conf_target - assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", node.bumpfee, txid, { - "confTarget": 123, - "conf_target": 456, - }) + def test_invalid_parameters(self, rbf_node, peer_node, dest_address): + self.log.info('Test invalid parameters') + rbfid = spend_one_input(rbf_node, dest_address) + self.sync_mempools((rbf_node, peer_node)) + assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() + + for key in ["totalFee", "feeRate"]: + assert_raises_rpc_error(-3, "Unexpected key {}".format(key), rbf_node.bumpfee, rbfid, {key: NORMAL}) + + # Bumping to just above minrelay should fail to increase the total fee enough. + assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT}) + + self.log.info("Test invalid fee rate settings") + assert_raises_rpc_error(-8, "Insufficient total fee 0.00", rbf_node.bumpfee, rbfid, {"fee_rate": 0}) + assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10", + rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) + assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) + for value in [{"foo": "bar"}, True]: + assert_raises_rpc_error(-3, "Amount is not a number or string", rbf_node.bumpfee, rbfid, {"fee_rate": value}) + assert_raises_rpc_error(-3, "Invalid amount", rbf_node.bumpfee, rbfid, {"fee_rate": ""}) + + self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") + assert_raises_rpc_error(-8, "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " + "target in blocks for automatic fee estimation, or an explicit fee rate.", + rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL}) + + self.log.info("Test explicit fee rate raises RPC error if both fee_rate and estimate_mode are passed") + assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate", + rbf_node.bumpfee, rbfid, {"estimate_mode": "economical", "fee_rate": NORMAL}) + + self.log.info("Test invalid conf_target settings") + assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", + rbf_node.bumpfee, rbfid, {"confTarget": 123, "conf_target": 456}) + + self.log.info("Test invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + rbf_node.bumpfee, rbfid, {"estimate_mode": v}) + for mode in ["foo", Decimal("3.1415"), "sat/B", "BTC/kB"]: + assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', + rbf_node.bumpfee, rbfid, {"estimate_mode": mode}) + self.clear_mempool() @@ -158,33 +177,13 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address): self.clear_mempool() -def test_feerate_args(self, rbf_node, peer_node, dest_address): - self.log.info('Test fee_rate args') - rbfid = spend_one_input(rbf_node, dest_address) - self.sync_mempools((rbf_node, peer_node)) - assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() - - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate": NORMAL, "confTarget": 1}) - - assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001, "confTarget": 1}) - - # Bumping to just above minrelay should fail to increase total fee enough, at least - assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT}) - - assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) - - assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) - self.clear_mempool() - - def test_segwit_bumpfee_succeeds(self, rbf_node, dest_address): self.log.info('Test that segwit-sourcing bumpfee works') # Create a transaction with segwit output, then create an RBF transaction # which spends it, and make sure bumpfee can be called on it. segwit_in = next(u for u in rbf_node.listunspent() if u["amount"] == Decimal("0.001")) - segwit_out = rbf_node.getaddressinfo(rbf_node.getnewaddress(address_type='p2sh-segwit')) + segwit_out = rbf_node.getaddressinfo(rbf_node.getnewaddress(address_type='bech32')) segwitid = send_to_witness( use_p2wsh=False, node=rbf_node, @@ -303,11 +302,11 @@ def test_dust_to_fee(self, rbf_node, dest_address): # boundary. Thus expected transaction size (p2wpkh, 1 input, 2 outputs) is 140-141 vbytes, usually 141. if not 140 <= fulltx["vsize"] <= 141: raise AssertionError("Invalid tx vsize of {} (140-141 expected), full tx: {}".format(fulltx["vsize"], fulltx)) - # Bump with fee_rate of 0.00350250 BTC per 1000 vbytes to create dust. + # Bump with fee_rate of 350.25 sat/vB vbytes to create dust. # Expected fee is 141 vbytes * fee_rate 0.00350250 BTC / 1000 vbytes = 0.00049385 BTC. # or occasionally 140 vbytes * fee_rate 0.00350250 BTC / 1000 vbytes = 0.00049035 BTC. # Dust should be dropped to the fee, so actual bump fee is 0.00050000 BTC. - bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": 0.00350250}) + bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": 350.25}) full_bumped_tx = rbf_node.getrawtransaction(bumped_tx["txid"], 1) assert_equal(bumped_tx["fee"], Decimal("0.00050000")) assert_equal(len(fulltx["vout"]), 2) @@ -348,7 +347,7 @@ def test_maxtxfee_fails(self, rbf_node, dest_address): self.restart_node(1, ['-maxtxfee=0.000025'] + self.extra_args[1]) rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) rbfid = spend_one_input(rbf_node, dest_address) - assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by -maxtxfee", rbf_node.bumpfee, rbfid) + assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", rbf_node.bumpfee, rbfid) self.restart_node(1, self.extra_args[1]) rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) self.connect_nodes(1, 0) @@ -365,7 +364,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): rbf_node.createwallet(wallet_name="signer", disable_private_keys=False, blank=True) signer = rbf_node.get_wallet_rpc("signer") assert signer.getwalletinfo()['private_keys_enabled'] - result = signer.importmulti([{ + reqs = [{ "desc": priv_rec_desc, "timestamp": 0, "range": [0,1], @@ -378,7 +377,11 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): "range": [0, 0], "internal": True, "keypool": False - }]) + }] + if self.options.descriptors: + result = signer.importdescriptors(reqs) + else: + result = signer.importmulti(reqs) assert_equal(result, [{'success': True}, {'success': True}]) # Create another wallet with just the public keys, which creates PSBTs @@ -386,21 +389,27 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): watcher = rbf_node.get_wallet_rpc("watcher") assert not watcher.getwalletinfo()['private_keys_enabled'] - result = watcher.importmulti([{ + reqs = [{ "desc": pub_rec_desc, "timestamp": 0, "range": [0, 10], "internal": False, "keypool": True, - "watchonly": True + "watchonly": True, + "active": True, }, { "desc": pub_change_desc, "timestamp": 0, "range": [0, 10], "internal": True, "keypool": True, - "watchonly": True - }]) + "watchonly": True, + "active": True, + }] + if self.options.descriptors: + result = watcher.importdescriptors(reqs) + else: + result = watcher.importmulti(reqs) assert_equal(result, [{'success': True}, {'success': True}]) funding_address1 = watcher.getnewaddress(address_type='bech32') @@ -410,7 +419,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): self.sync_all() # Create single-input PSBT for transaction to be bumped - psbt = watcher.walletcreatefundedpsbt([], {dest_address: 0.0005}, 0, {"feeRate": 0.00001}, True)['psbt'] + psbt = watcher.walletcreatefundedpsbt([], {dest_address: 0.0005}, 0, {"fee_rate": 1}, True)['psbt'] psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True) psbt_final = watcher.finalizepsbt(psbt_signed["psbt"]) original_txid = watcher.sendrawtransaction(psbt_final["hex"]) diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py index ed9159726a..0f11aca525 100755 --- a/test/functional/wallet_create_tx.py +++ b/test/functional/wallet_create_tx.py @@ -53,12 +53,12 @@ class CreateTxWalletTest(BitcoinTestFramework): self.restart_node(0, extra_args=[fee_setting]) assert_raises_rpc_error( -6, - "Fee exceeds maximum configured by -maxtxfee", + "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", lambda: self.nodes[0].sendmany(dummy="", amounts=outputs), ) assert_raises_rpc_error( -4, - "Fee exceeds maximum configured by -maxtxfee", + "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx), ) @@ -67,12 +67,12 @@ class CreateTxWalletTest(BitcoinTestFramework): self.nodes[0].settxfee(0.01) assert_raises_rpc_error( -6, - "Fee exceeds maximum configured by -maxtxfee", + "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", lambda: self.nodes[0].sendmany(dummy="", amounts=outputs), ) assert_raises_rpc_error( -4, - "Fee exceeds maximum configured by -maxtxfee", + "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx), ) self.nodes[0].settxfee(0) diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index 48c0fcb731..cf3317121f 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -5,11 +5,15 @@ """Test createwallet arguments. """ +from test_framework.address import key_to_p2wpkh +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, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif, generate_wif_key class CreateWalletTest(BitcoinTestFramework): def set_test_params(self): @@ -35,10 +39,14 @@ class CreateWalletTest(BitcoinTestFramework): w1.importpubkey(w0.getaddressinfo(address1)['pubkey']) self.log.info('Test that private keys cannot be imported') - addr = w0.getnewaddress('', 'legacy') - privkey = w0.dumpprivkey(addr) + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) assert_raises_rpc_error(-4, 'Cannot import private keys to a wallet with private keys disabled', w1.importprivkey, privkey) - result = w1.importmulti([{'scriptPubKey': {'address': addr}, 'timestamp': 'now', 'keys': [privkey]}]) + if self.options.descriptors: + result = w1.importdescriptors([{'desc': descsum_create('wpkh(' + privkey + ')'), 'timestamp': 'now'}]) + else: + result = w1.importmulti([{'scriptPubKey': {'address': key_to_p2wpkh(eckey.get_pubkey().get_bytes())}, 'timestamp': 'now', 'keys': [privkey]}]) assert not result[0]['success'] assert 'warning' not in result[0] assert_equal(result[0]['error']['code'], -4) @@ -58,12 +66,25 @@ class CreateWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getrawchangeaddress) # Import private key - w3.importprivkey(w0.dumpprivkey(address1)) + w3.importprivkey(generate_wif_key()) # Imported private keys are currently ignored by the keypool assert_equal(w3.getwalletinfo()['keypoolsize'], 0) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) # Set the seed - w3.sethdseed() + if self.options.descriptors: + w3.importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + else: + w3.sethdseed() assert_equal(w3.getwalletinfo()['keypoolsize'], 1) w3.getnewaddress() w3.getrawchangeaddress() @@ -80,7 +101,20 @@ class CreateWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w4.getrawchangeaddress) # Now set a seed and it should work. Wallet should also be encrypted w4.walletpassphrase('pass', 60) - w4.sethdseed() + if self.options.descriptors: + w4.importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + else: + w4.sethdseed() w4.getnewaddress() w4.getrawchangeaddress() @@ -111,13 +145,14 @@ class CreateWalletTest(BitcoinTestFramework): w6.walletpassphrase('thisisapassphrase', 60) w6.signmessage(w6.getnewaddress('', 'legacy'), "test") w6.keypoolrefill(1) - # There should only be 1 key + # There should only be 1 key for legacy, 3 for descriptors walletinfo = w6.getwalletinfo() - assert_equal(walletinfo['keypoolsize'], 1) - assert_equal(walletinfo['keypoolsize_hd_internal'], 1) + keys = 3 if self.options.descriptors else 1 + assert_equal(walletinfo['keypoolsize'], keys) + assert_equal(walletinfo['keypoolsize_hd_internal'], keys) # Allow empty passphrase, but there should be a warning resp = self.nodes[0].createwallet(wallet_name='w7', disable_private_keys=False, blank=False, passphrase='') - assert_equal(resp['warning'], 'Empty string given as passphrase, wallet will not be encrypted.') + assert 'Empty string given as passphrase, wallet will not be encrypted.' in resp['warning'] w7 = node.get_wallet_rpc('w7') assert_raises_rpc_error(-15, 'Error: running with an unencrypted wallet, but walletpassphrase was called.', w7.walletpassphrase, '', 60) diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index 9c63e8f7d3..0fec8ea4a4 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -16,19 +16,27 @@ class WalletDescriptorTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-keypool=100']] + self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() + self.skip_if_no_sqlite() def run_test(self): + # Make a legacy wallet and check it is BDB + self.nodes[0].createwallet(wallet_name="legacy1", descriptors=False) + wallet_info = self.nodes[0].getwalletinfo() + assert_equal(wallet_info['format'], 'bdb') + self.nodes[0].unloadwallet("legacy1") + # Make a descriptor wallet self.log.info("Making a descriptor wallet") self.nodes[0].createwallet(wallet_name="desc1", descriptors=True) - self.nodes[0].unloadwallet("") # A descriptor wallet should have 100 addresses * 3 types = 300 keys self.log.info("Checking wallet info") wallet_info = self.nodes[0].getwalletinfo() + assert_equal(wallet_info['format'], 'sqlite') assert_equal(wallet_info['keypoolsize'], 300) assert_equal(wallet_info['keypoolsize_hd_internal'], 300) assert 'keypoololdest' not in wallet_info diff --git a/test/functional/wallet_disable.py b/test/functional/wallet_disable.py index 7c2ec56b5a..c2b30fb35b 100755 --- a/test/functional/wallet_disable.py +++ b/test/functional/wallet_disable.py @@ -16,6 +16,7 @@ class DisableWalletTest (BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-disablewallet"]] + self.wallet_names = [] def run_test (self): # Make sure wallet is really disabled diff --git a/test/functional/wallet_dump.py b/test/functional/wallet_dump.py index 09581d864b..eb54da99f5 100755 --- a/test/functional/wallet_dump.py +++ b/test/functional/wallet_dump.py @@ -95,7 +95,7 @@ def read_dump(file_name, addrs, script_addrs, hd_master_addr_old): class WalletDumpTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [["-keypool=90", "-addresstype=legacy", "-wallet=dump"]] + self.extra_args = [["-keypool=90", "-addresstype=legacy"]] self.rpc_timeout = 120 def skip_test_if_missing_module(self): @@ -106,6 +106,8 @@ class WalletDumpTest(BitcoinTestFramework): self.start_nodes() def run_test(self): + self.nodes[0].createwallet("dump") + wallet_unenc_dump = os.path.join(self.nodes[0].datadir, "wallet.unencrypted.dump") wallet_enc_dump = os.path.join(self.nodes[0].datadir, "wallet.encrypted.dump") @@ -190,7 +192,8 @@ class WalletDumpTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "already exists", lambda: self.nodes[0].dumpwallet(wallet_enc_dump)) # Restart node with new wallet, and test importwallet - self.restart_node(0, ['-wallet=w2']) + self.restart_node(0) + self.nodes[0].createwallet("w2") # Make sure the address is not IsMine before import result = self.nodes[0].getaddressinfo(multisig_addr) diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index 3c336623e2..d45cf05689 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -10,7 +10,6 @@ import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, assert_raises_rpc_error, ) @@ -84,7 +83,7 @@ class WalletHDTest(BitcoinTestFramework): shutil.rmtree(os.path.join(self.nodes[1].datadir, self.chain, "chainstate")) shutil.copyfile( os.path.join(self.nodes[1].datadir, "hd.bak"), - os.path.join(self.nodes[1].datadir, self.chain, 'wallets', "wallet.dat"), + os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename), ) self.start_node(1) @@ -99,7 +98,7 @@ class WalletHDTest(BitcoinTestFramework): assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/" + str(i) + "'") assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint) assert_equal(hd_add, hd_add_2) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_all() # Needs rescan @@ -112,10 +111,10 @@ class WalletHDTest(BitcoinTestFramework): shutil.rmtree(os.path.join(self.nodes[1].datadir, self.chain, "chainstate")) shutil.copyfile( os.path.join(self.nodes[1].datadir, "hd.bak"), - os.path.join(self.nodes[1].datadir, self.chain, "wallets", "wallet.dat"), + os.path.join(self.nodes[1].datadir, self.chain, "wallets", self.default_wallet_name, self.wallet_data_filename), ) self.start_node(1, extra_args=self.extra_args[1]) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) self.sync_all() # Wallet automatically scans blocks older than key on startup assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1) @@ -183,7 +182,7 @@ class WalletHDTest(BitcoinTestFramework): # Restart node 1 with keypool of 3 and a different wallet self.nodes[1].createwallet(wallet_name='origin', blank=True) self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin']) - connect_nodes(self.nodes[0], 1) + self.connect_nodes(0, 1) # sethdseed restoring and seeing txs to addresses out of the keypool origin_rpc = self.nodes[1].get_wallet_rpc('origin') diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index 87deaded09..aad112b499 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -22,7 +22,6 @@ happened previously. from test_framework.test_framework import BitcoinTestFramework from test_framework.address import AddressType from test_framework.util import ( - connect_nodes, assert_equal, set_node_times, ) @@ -151,7 +150,7 @@ class ImportRescanTest(BitcoinTestFramework): self.skip_if_no_wallet() def setup_network(self): - self.extra_args = [["-wallet="] for _ in range(self.num_nodes)] + self.extra_args = [[] for _ in range(self.num_nodes)] for i, import_node in enumerate(IMPORT_NODES, 2): if import_node.prune: self.extra_args[i] += ["-prune=1"] @@ -159,14 +158,13 @@ class ImportRescanTest(BitcoinTestFramework): self.add_nodes(self.num_nodes, extra_args=self.extra_args) # Import keys with pruning disabled - self.start_nodes(extra_args=[["-wallet="]] * self.num_nodes) - for n in self.nodes: - n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase') + self.start_nodes(extra_args=[[]] * self.num_nodes) + self.import_deterministic_coinbase_privkeys() self.stop_nodes() self.start_nodes() for i in range(1, self.num_nodes): - connect_nodes(self.nodes[i], 0) + self.connect_nodes(i, 0) def run_test(self): # Create one transaction on node 0 with a unique amount for @@ -183,6 +181,7 @@ class ImportRescanTest(BitcoinTestFramework): self.nodes[0].generate(1) # Generate one block for each send variant.confirmation_height = self.nodes[0].getblockcount() variant.timestamp = self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"] + self.sync_all() # Conclude sync before calling setmocktime to avoid timeouts # Generate a block further in the future (past the rescan window). assert_equal(self.nodes[0].getrawmempool(), []) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 2d982edef8..2903a84998 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -15,6 +15,7 @@ variants. - `test_address()` is called to call getaddressinfo for an address on node1 and test the values returned.""" +from test_framework.address import key_to_p2pkh from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import descsum_create from test_framework.util import ( @@ -34,9 +35,11 @@ class ImportDescriptorsTest(BitcoinTestFramework): ["-addresstype=bech32", "-keypool=5"] ] self.setup_clean_chain = True + self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() + self.skip_if_no_sqlite() def test_importdesc(self, req, success, error_code=None, error_message=None, warnings=None, wallet=None): """Run importdescriptors and assert success""" @@ -58,7 +61,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): def run_test(self): self.log.info('Setting up wallets') - self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False) + self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False, descriptors=True) w0 = self.nodes[0].get_wallet_rpc('w0') self.nodes[1].createwallet(wallet_name='w1', disable_private_keys=True, blank=True, descriptors=True) @@ -105,6 +108,17 @@ class ImportDescriptorsTest(BitcoinTestFramework): error_code=-8, error_message="Internal addresses should not have a label") + self.log.info("Internal addresses should be detected as such") + key = get_generate_key() + addr = key_to_p2pkh(key.pubkey) + self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), + "timestamp": "now", + "internal": True}, + success=True) + info = w1.getaddressinfo(addr) + assert_equal(info["ismine"], True) + assert_equal(info["ischange"], True) + # # Test importing of a P2SH-P2WPKH descriptor key = get_generate_key() self.log.info("Should not import a p2sh-p2wpkh descriptor without checksum") @@ -207,6 +221,15 @@ class ImportDescriptorsTest(BitcoinTestFramework): success=False, error_code=-4, error_message='Cannot import private keys to a wallet with private keys disabled') + + self.log.info("Should not import a descriptor with hardened derivations when private keys are disabled") + self.test_importdesc({"desc": descsum_create("wpkh(" + xpub + "/1h/*)"), + "timestamp": "now", + "range": 1}, + success=False, + error_code=-4, + error_message='Cannot expand descriptor. Probably because of hardened derivations without private keys provided') + for address in addresses: test_address(w1, address, diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index bd4fcdabcf..30206349ae 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -64,7 +64,7 @@ class ImportMultiTest(BitcoinTestFramework): self.nodes[0].generate(1) self.nodes[1].generate(1) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() + self.nodes[1].syncwithvalidationinterfacequeue() # Sync the timestamp to the wallet, so that importmulti works node0_address1 = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress()) @@ -820,7 +820,7 @@ class ImportMultiTest(BitcoinTestFramework): # Cannot import those pubkeys to keypool of wallet with privkeys self.log.info("Pubkeys cannot be added to the keypool of a wallet with private keys") - wrpc = self.nodes[1].get_wallet_rpc("") + wrpc = self.nodes[1].get_wallet_rpc(self.default_wallet_name) assert wrpc.getwalletinfo()['private_keys_enabled'] result = wrpc.importmulti( [{ diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 78426018ef..d10d8ae1c8 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -5,11 +5,14 @@ """Test the importprunedfunds and removeprunedfunds RPCs.""" from decimal import Decimal +from test_framework.address import key_to_p2wpkh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif class ImportPrunedFundsTest(BitcoinTestFramework): def set_test_params(self): @@ -30,8 +33,11 @@ class ImportPrunedFundsTest(BitcoinTestFramework): # pubkey address2 = self.nodes[0].getnewaddress() # privkey - address3 = self.nodes[0].getnewaddress() - address3_privkey = self.nodes[0].dumpprivkey(address3) # Using privkey + eckey = ECKey() + eckey.generate() + address3_privkey = bytes_to_wif(eckey.get_bytes()) + address3 = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + self.nodes[0].importprivkey(address3_privkey) # Check only one address address_info = self.nodes[0].getaddressinfo(address1) @@ -80,37 +86,44 @@ class ImportPrunedFundsTest(BitcoinTestFramework): assert_equal(balance1, Decimal(0)) # Import with affiliated address with no rescan - self.nodes[1].importaddress(address=address2, rescan=False) - self.nodes[1].importprunedfunds(rawtransaction=rawtxn2, txoutproof=proof2) - assert [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid2] + self.nodes[1].createwallet('wwatch', disable_private_keys=True) + wwatch = self.nodes[1].get_wallet_rpc('wwatch') + wwatch.importaddress(address=address2, rescan=False) + wwatch.importprunedfunds(rawtransaction=rawtxn2, txoutproof=proof2) + assert [tx for tx in wwatch.listtransactions(include_watchonly=True) if tx['txid'] == txnid2] # Import with private key with no rescan - self.nodes[1].importprivkey(privkey=address3_privkey, rescan=False) - self.nodes[1].importprunedfunds(rawtxn3, proof3) - assert [tx for tx in self.nodes[1].listtransactions() if tx['txid'] == txnid3] - balance3 = self.nodes[1].getbalance() + w1 = self.nodes[1].get_wallet_rpc(self.default_wallet_name) + w1.importprivkey(privkey=address3_privkey, rescan=False) + w1.importprunedfunds(rawtxn3, proof3) + assert [tx for tx in w1.listtransactions() if tx['txid'] == txnid3] + balance3 = w1.getbalance() assert_equal(balance3, Decimal('0.025')) # Addresses Test - after import - address_info = self.nodes[1].getaddressinfo(address1) + address_info = w1.getaddressinfo(address1) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], False) - address_info = self.nodes[1].getaddressinfo(address2) - assert_equal(address_info['iswatchonly'], True) - assert_equal(address_info['ismine'], False) - address_info = self.nodes[1].getaddressinfo(address3) + address_info = wwatch.getaddressinfo(address2) + if self.options.descriptors: + assert_equal(address_info['iswatchonly'], False) + assert_equal(address_info['ismine'], True) + else: + assert_equal(address_info['iswatchonly'], True) + assert_equal(address_info['ismine'], False) + address_info = w1.getaddressinfo(address3) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], True) # Remove transactions - assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", self.nodes[1].removeprunedfunds, txnid1) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid1] + assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", w1.removeprunedfunds, txnid1) + assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid1] - self.nodes[1].removeprunedfunds(txnid2) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid2] + wwatch.removeprunedfunds(txnid2) + assert not [tx for tx in wwatch.listtransactions(include_watchonly=True) if tx['txid'] == txnid2] - self.nodes[1].removeprunedfunds(txnid3) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid3] + w1.removeprunedfunds(txnid3) + assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid3] if __name__ == '__main__': ImportPrunedFundsTest().main() diff --git a/test/functional/wallet_keypool.py b/test/functional/wallet_keypool.py index 40a2b3ab6a..51795aca23 100755 --- a/test/functional/wallet_keypool.py +++ b/test/functional/wallet_keypool.py @@ -143,7 +143,7 @@ class KeyPoolTest(BitcoinTestFramework): w2 = nodes[0].get_wallet_rpc('w2') # refer to initial wallet as w1 - w1 = nodes[0].get_wallet_rpc('') + w1 = nodes[0].get_wallet_rpc(self.default_wallet_name) # import private key and fund it address = addr.pop() diff --git a/test/functional/wallet_keypool_topup.py b/test/functional/wallet_keypool_topup.py index 102ed23fba..78e06c5916 100755 --- a/test/functional/wallet_keypool_topup.py +++ b/test/functional/wallet_keypool_topup.py @@ -16,7 +16,6 @@ import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, ) @@ -30,7 +29,7 @@ class KeypoolRestoreTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - wallet_path = os.path.join(self.nodes[1].datadir, self.chain, "wallets", "wallet.dat") + wallet_path = os.path.join(self.nodes[1].datadir, self.chain, "wallets", self.default_wallet_name, self.wallet_data_filename) wallet_backup_path = os.path.join(self.nodes[1].datadir, "wallet.bak") self.nodes[0].generate(101) @@ -38,9 +37,9 @@ class KeypoolRestoreTest(BitcoinTestFramework): self.stop_node(1) shutil.copyfile(wallet_path, wallet_backup_path) self.start_node(1, self.extra_args[1]) - connect_nodes(self.nodes[0], 1) - connect_nodes(self.nodes[0], 2) - connect_nodes(self.nodes[0], 3) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + self.connect_nodes(0, 3) for i, output_type in enumerate(["legacy", "p2sh-segwit", "bech32"]): @@ -72,7 +71,7 @@ class KeypoolRestoreTest(BitcoinTestFramework): self.stop_node(idx) shutil.copyfile(wallet_backup_path, wallet_path) self.start_node(idx, self.extra_args[idx]) - connect_nodes(self.nodes[0], idx) + self.connect_nodes(0, idx) self.sync_all() self.log.info("Verify keypool is restored and balance is correct") diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py index cff59bd1c1..883b97561e 100755 --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -135,7 +135,7 @@ class WalletLabelsTest(BitcoinTestFramework): change_label(node, labels[2].addresses[0], labels[2], labels[2]) self.log.info('Check watchonly labels') - node.createwallet(wallet_name='watch_only', disable_private_keys=True, descriptors=False) + node.createwallet(wallet_name='watch_only', disable_private_keys=True) wallet_watch_only = node.get_wallet_rpc('watch_only') BECH32_VALID = { '✔️_VER15_PROG40': 'bcrt10qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn2cjv3', @@ -156,7 +156,7 @@ class WalletLabelsTest(BitcoinTestFramework): ad = BECH32_INVALID[l] assert_raises_rpc_error( -5, - "Invalid Bitcoin address or script", + "Address is not valid" if self.options.descriptors else "Invalid Bitcoin address or script", lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad), ) diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index d4131deabf..6a1b9097c5 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -4,14 +4,16 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listsinceblock RPC.""" +from test_framework.address import key_to_p2wpkh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import BIP125_SEQUENCE_NUMBER from test_framework.util import ( assert_array_result, assert_equal, assert_raises_rpc_error, - connect_nodes, ) +from test_framework.wallet_util import bytes_to_wif from decimal import Decimal @@ -26,7 +28,7 @@ class ListSinceBlockTest(BitcoinTestFramework): def run_test(self): # All nodes are in IBD from genesis, so they'll need the miner (node2) to be an outbound connection, or have # only one connection. (See fPreferredDownload in net_processing) - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) self.nodes[2].generate(101) self.sync_all() @@ -182,15 +184,22 @@ class ListSinceBlockTest(BitcoinTestFramework): self.sync_all() - # Split network into two - self.split_network() - # share utxo between nodes[1] and nodes[2] + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + address = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + self.nodes[2].sendtoaddress(address, 10) + self.nodes[2].generate(6) + self.sync_all() + self.nodes[2].importprivkey(privkey) utxos = self.nodes[2].listunspent() - utxo = utxos[0] - privkey = self.nodes[2].dumpprivkey(utxo['address']) + utxo = [u for u in utxos if u["address"] == address][0] self.nodes[1].importprivkey(privkey) + # Split network into two + self.split_network() + # send from nodes[1] using utxo to nodes[0] change = '%.8f' % (float(utxo['amount']) - 1.0003) recipient_dict = { diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 8ff663ccd2..3e98208431 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -91,18 +91,20 @@ class ListTransactionsTest(BitcoinTestFramework): {"category": "receive", "amount": Decimal("0.44")}, {"txid": txid}) - pubkey = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey'] - multisig = self.nodes[1].createmultisig(1, [pubkey]) - self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) - txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) - self.nodes[1].generate(1) - self.sync_all() - assert_equal(len(self.nodes[0].listtransactions(label="watchonly", include_watchonly=True)), 1) - assert_equal(len(self.nodes[0].listtransactions(dummy="watchonly", include_watchonly=True)), 1) - assert len(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=False)) == 0 - assert_array_result(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=True), - {"category": "receive", "amount": Decimal("0.1")}, - {"txid": txid, "label": "watchonly"}) + if not self.options.descriptors: + # include_watchonly is a legacy wallet feature, so don't test it for descriptor wallets + pubkey = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey'] + multisig = self.nodes[1].createmultisig(1, [pubkey]) + self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) + txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) + self.nodes[1].generate(1) + self.sync_all() + assert_equal(len(self.nodes[0].listtransactions(label="watchonly", include_watchonly=True)), 1) + assert_equal(len(self.nodes[0].listtransactions(dummy="watchonly", include_watchonly=True)), 1) + assert len(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=False)) == 0 + assert_array_result(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=True), + {"category": "receive", "amount": Decimal("0.1")}, + {"txid": txid, "label": "watchonly"}) self.run_rbf_opt_in_test() diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index f0be271c66..fb4532bcf6 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -10,6 +10,7 @@ from decimal import Decimal from threading import Thread import os import shutil +import stat import time from test_framework.authproxy import JSONRPCException @@ -41,7 +42,7 @@ class MultiWalletTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 2 self.rpc_timeout = 120 - self.extra_args = [["-wallet="], ["-wallet="]] + self.extra_args = [["-nowallet"], []] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -61,30 +62,44 @@ class MultiWalletTest(BitcoinTestFramework): wallet = lambda name: node.get_wallet_rpc(name) def wallet_file(name): + if name == self.default_wallet_name: + return wallet_dir(self.default_wallet_name, self.wallet_data_filename) if os.path.isdir(wallet_dir(name)): return wallet_dir(name, "wallet.dat") return wallet_dir(name) - assert_equal(self.nodes[0].listwalletdir(), { 'wallets': [{ 'name': '' }] }) + assert_equal(self.nodes[0].listwalletdir(), { 'wallets': [{ 'name': self.default_wallet_name }] }) # check wallet.dat is created self.stop_nodes() - assert_equal(os.path.isfile(wallet_dir('wallet.dat')), True) + assert_equal(os.path.isfile(wallet_dir(self.default_wallet_name, self.wallet_data_filename)), True) # create symlink to verify wallet directory path can be referenced # through symlink os.mkdir(wallet_dir('w7')) os.symlink('w7', wallet_dir('w7_symlink')) + os.symlink('..', wallet_dir('recursive_dir_symlink')) + + os.mkdir(wallet_dir('self_walletdat_symlink')) + os.symlink('wallet.dat', wallet_dir('self_walletdat_symlink/wallet.dat')) + # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded - os.rename(wallet_dir("wallet.dat"), wallet_dir("w8")) - # create another dummy wallet for use in testing backups later - self.start_node(0, ["-wallet="]) + self.start_node(0) + node.createwallet("empty") + node.createwallet("plain") + node.createwallet("created") self.stop_nodes() empty_wallet = os.path.join(self.options.tmpdir, 'empty.dat') - os.rename(wallet_dir("wallet.dat"), empty_wallet) + os.rename(wallet_file("empty"), empty_wallet) + shutil.rmtree(wallet_dir("empty")) + empty_created_wallet = os.path.join(self.options.tmpdir, 'empty.created.dat') + os.rename(wallet_dir("created", self.wallet_data_filename), empty_created_wallet) + shutil.rmtree(wallet_dir("created")) + os.rename(wallet_file("plain"), wallet_dir("w8")) + shutil.rmtree(wallet_dir("plain")) # restart node with a mix of wallet names: # w1, w2, w3 - to verify new wallets created when non-existing paths specified @@ -92,35 +107,58 @@ class MultiWalletTest(BitcoinTestFramework): # sub/w5 - to verify relative wallet path is created correctly # extern/w6 - to verify absolute wallet path is created correctly # w7_symlink - to verify symlinked wallet path is initialized correctly - # w8 - to verify existing wallet file is loaded correctly + # w8 - to verify existing wallet file is loaded correctly. Not tested for SQLite wallets as this is a deprecated BDB behavior. # '' - to verify default wallet file is created correctly - wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5', os.path.join(self.options.tmpdir, 'extern/w6'), 'w7_symlink', 'w8', ''] - extra_args = ['-wallet={}'.format(n) for n in wallet_names] - self.start_node(0, extra_args) - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), ['', os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8']) + to_create = ['w1', 'w2', 'w3', 'w', 'sub/w5', 'w7_symlink'] + in_wallet_dir = [w.replace('/', os.path.sep) for w in to_create] # Wallets in the wallet dir + in_wallet_dir.append('w7') # w7 is not loaded or created, but will be listed by listwalletdir because w7_symlink + to_create.append(os.path.join(self.options.tmpdir, 'extern/w6')) # External, not in the wallet dir, so we need to avoid adding it to in_wallet_dir + to_load = [self.default_wallet_name] + if not self.options.descriptors: + to_load.append('w8') + wallet_names = to_create + to_load # Wallet names loaded in the wallet + in_wallet_dir += to_load # The loaded wallets are also in the wallet dir + self.start_node(0) + for wallet_name in to_create: + self.nodes[0].createwallet(wallet_name) + for wallet_name in to_load: + self.nodes[0].loadwallet(wallet_name) + + os.mkdir(wallet_dir('no_access')) + os.chmod(wallet_dir('no_access'), 0) + try: + with self.nodes[0].assert_debug_log(expected_msgs=['Error scanning']): + walletlist = self.nodes[0].listwalletdir()['wallets'] + finally: + # Need to ensure access is restored for cleanup + os.chmod(wallet_dir('no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) assert_equal(set(node.listwallets()), set(wallet_names)) + # should raise rpc error if wallet path can't be created + err_code = -4 if self.options.descriptors else -1 + assert_raises_rpc_error(err_code, "boost::filesystem::create_directory:", self.nodes[0].createwallet, "w8/bad") + # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: assert_equal(os.path.isfile(wallet_file(wallet_name)), True) - # should not initialize if wallet path can't be created - exp_stderr = "boost::filesystem::create_directory:" - self.nodes[0].assert_start_raises_init_error(['-wallet=wallet.dat/bad'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) - self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir()) self.nodes[0].assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir()) - # should not initialize if there are duplicate wallets - self.nodes[0].assert_start_raises_init_error(['-wallet=w1', '-wallet=w1'], 'Error: Error loading wallet w1. Duplicate -wallet filename specified.') + self.start_node(0, ['-wallet=w1', '-wallet=w1']) + self.stop_node(0, 'Warning: Ignoring duplicate -wallet w1.') - # should not initialize if one wallet is a copy of another - shutil.copyfile(wallet_dir('w8'), wallet_dir('w8_copy')) - exp_stderr = r"BerkeleyDatabase: Can't open database w8_copy \(duplicates fileid \w+ from w8\)" - self.nodes[0].assert_start_raises_init_error(['-wallet=w8', '-wallet=w8_copy'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) + if not self.options.descriptors: + # Only BDB doesn't open duplicate wallet files. SQLite does not have this limitation. While this may be desired in the future, it is not necessary + # should not initialize if one wallet is a copy of another + shutil.copyfile(wallet_dir('w8'), wallet_dir('w8_copy')) + in_wallet_dir.append('w8_copy') + exp_stderr = r"BerkeleyDatabase: Can't open database w8_copy \(duplicates fileid \w+ from w8\)" + self.nodes[0].assert_start_raises_init_error(['-wallet=w8', '-wallet=w8_copy'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) # should not initialize if wallet file is a symlink os.symlink('w8', wallet_dir('w8_symlink')) @@ -133,17 +171,24 @@ class MultiWalletTest(BitcoinTestFramework): open(not_a_dir, 'a', encoding="utf8").close() self.nodes[0].assert_start_raises_init_error(['-walletdir=' + not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory') + self.log.info("Do not allow -upgradewallet with multiwallet") + self.nodes[0].assert_start_raises_init_error(['-upgradewallet'], "Error: Error parsing command line arguments: Invalid parameter -upgradewallet") + # if wallets/ doesn't exist, datadir should be the default wallet dir wallet_dir2 = data_dir('walletdir') os.rename(wallet_dir(), wallet_dir2) - self.start_node(0, ['-wallet=w4', '-wallet=w5']) + self.start_node(0) + self.nodes[0].createwallet("w4") + self.nodes[0].createwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") node.generatetoaddress(nblocks=1, address=w5.getnewaddress()) # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded os.rename(wallet_dir2, wallet_dir()) - self.restart_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + data_dir()]) + self.restart_node(0, ['-nowallet', '-walletdir=' + data_dir()]) + self.nodes[0].loadwallet("w4") + self.nodes[0].loadwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") w5_info = w5.getwalletinfo() @@ -151,13 +196,19 @@ class MultiWalletTest(BitcoinTestFramework): competing_wallet_dir = os.path.join(self.options.tmpdir, 'competing_walletdir') os.mkdir(competing_wallet_dir) - self.restart_node(0, ['-walletdir=' + competing_wallet_dir, '-wallet=']) - exp_stderr = r"Error: Error initializing wallet database environment \"\S+competing_walletdir\"!" + 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?" + 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) - self.restart_node(0, extra_args) + self.restart_node(0) + for wallet_name in wallet_names: + self.nodes[0].loadwallet(wallet_name) - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), ['', os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy']) + assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) wallets = [wallet(w) for w in wallet_names] wallet_bad = wallet("bad") @@ -205,7 +256,7 @@ class MultiWalletTest(BitcoinTestFramework): self.restart_node(0, ['-nowallet']) assert_equal(node.listwallets(), []) - assert_raises_rpc_error(-32601, "Method not found", node.getwalletinfo) + assert_raises_rpc_error(-18, "No wallet is loaded. Load a wallet using loadwallet or create a new one with createwallet. (Note: A default wallet is no longer automatically created)", node.getwalletinfo) self.log.info("Load first wallet") loadwallet_name = node.loadwallet(wallet_names[0]) @@ -248,18 +299,22 @@ class MultiWalletTest(BitcoinTestFramework): # Fail to load duplicate wallets path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w1", "wallet.dat") - assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) - - # Fail to load duplicate wallets by different ways (directory and filepath) - path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "wallet.dat") - assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, '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]) + else: + assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) - # Fail to load if one wallet is a copy of another - assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # This tests the default wallet that BDB makes, so SQLite wallet doesn't need to test this + # Fail to load duplicate wallets by different ways (directory and filepath) + path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "wallet.dat") + assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, 'wallet.dat') - # Fail to load if one wallet is a copy of another, test this twice to make sure that we don't re-introduce #14304 - assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # Only BDB doesn't open duplicate wallet files. SQLite does not have this limitation. While this may be desired in the future, it is not necessary + # Fail to load if one wallet is a copy of another + assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # Fail to load if one wallet is a copy of another, test this twice to make sure that we don't re-introduce #14304 + assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') # Fail to load if wallet file is a symlink assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') @@ -277,6 +332,7 @@ class MultiWalletTest(BitcoinTestFramework): # Successfully create a wallet with a new name loadwallet_name = self.nodes[0].createwallet('w9') + in_wallet_dir.append('w9') assert_equal(loadwallet_name['name'], 'w9') w9 = node.get_wallet_rpc('w9') assert_equal(w9.getwalletinfo()['walletname'], 'w9') @@ -299,12 +355,18 @@ class MultiWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].unloadwallet) assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", self.nodes[0].unloadwallet, "dummy") assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.get_wallet_rpc("dummy").unloadwallet) - assert_raises_rpc_error(-8, "Cannot unload the requested wallet", w1.unloadwallet, "w2"), + assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", w1.unloadwallet, "w2"), # Successfully unload the specified wallet name self.nodes[0].unloadwallet("w1") assert 'w1' not in self.nodes[0].listwallets() + # Unload w1 again, this time providing the wallet name twice + self.nodes[0].loadwallet("w1") + assert 'w1' in self.nodes[0].listwallets() + w1.unloadwallet("w1") + assert 'w1' not in self.nodes[0].listwallets() + # Successfully unload the wallet referenced by the request endpoint # Also ensure unload works during walletpassphrase timeout w2.encryptwallet('test') @@ -317,14 +379,14 @@ class MultiWalletTest(BitcoinTestFramework): for wallet_name in self.nodes[0].listwallets(): self.nodes[0].unloadwallet(wallet_name) assert_equal(self.nodes[0].listwallets(), []) - assert_raises_rpc_error(-32601, "Method not found (wallet method is disabled because no wallet is loaded)", self.nodes[0].getwalletinfo) + assert_raises_rpc_error(-18, "No wallet is loaded. Load a wallet using loadwallet or create a new one with createwallet. (Note: A default wallet is no longer automatically created)", self.nodes[0].getwalletinfo) # Successfully load a previously unloaded wallet self.nodes[0].loadwallet('w1') assert_equal(self.nodes[0].listwallets(), ['w1']) assert_equal(w1.getwalletinfo()['walletname'], 'w1') - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), ['', os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy', 'w9']) + assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) # Test backing up and restoring wallets self.log.info("Test wallet backup") @@ -335,9 +397,11 @@ class MultiWalletTest(BitcoinTestFramework): rpc = self.nodes[0].get_wallet_rpc(wallet_name) addr = rpc.getnewaddress() backup = os.path.join(self.options.tmpdir, 'backup.dat') + if os.path.exists(backup): + os.unlink(backup) rpc.backupwallet(backup) self.nodes[0].unloadwallet(wallet_name) - shutil.copyfile(empty_wallet, wallet_file(wallet_name)) + shutil.copyfile(empty_created_wallet if wallet_name == self.default_wallet_name else empty_wallet, wallet_file(wallet_name)) self.nodes[0].loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], False) self.nodes[0].unloadwallet(wallet_name) @@ -349,9 +413,13 @@ class MultiWalletTest(BitcoinTestFramework): self.start_node(1) wallet = os.path.join(self.options.tmpdir, 'my_wallet') self.nodes[0].createwallet(wallet) - assert_raises_rpc_error(-4, "Error initializing wallet database environment", self.nodes[1].loadwallet, wallet) + if self.options.descriptors: + assert_raises_rpc_error(-4, "Unable to obtain an exclusive lock", self.nodes[1].loadwallet, wallet) + else: + assert_raises_rpc_error(-4, "Error initializing wallet database environment", self.nodes[1].loadwallet, wallet) self.nodes[0].unloadwallet(wallet) self.nodes[1].loadwallet(wallet) + if __name__ == '__main__': MultiWalletTest().main() diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index 455f1fc5e8..a1d6b098ad 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -20,8 +20,6 @@ import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, ) class ReorgsRestoreTest(BitcoinTestFramework): @@ -38,9 +36,9 @@ class ReorgsRestoreTest(BitcoinTestFramework): self.sync_blocks() # Disconnect node1 from others to reorg its chain later - disconnect_nodes(self.nodes[0], 1) - disconnect_nodes(self.nodes[1], 2) - connect_nodes(self.nodes[0], 2) + self.disconnect_nodes(0, 1) + self.disconnect_nodes(1, 2) + self.connect_nodes(0, 2) # Send a tx to be unconfirmed later txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) @@ -50,7 +48,7 @@ class ReorgsRestoreTest(BitcoinTestFramework): assert_equal(tx_before_reorg["confirmations"], 4) # Disconnect node0 from node2 to broadcast a conflict on their respective chains - disconnect_nodes(self.nodes[0], 2) + self.disconnect_nodes(0, 2) nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10")) inputs = [] inputs.append({"txid": txid_conflict_from, "vout": nA}) @@ -69,7 +67,7 @@ class ReorgsRestoreTest(BitcoinTestFramework): self.nodes[2].generate(9) # Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted - connect_nodes(self.nodes[0], 2) + self.connect_nodes(0, 2) self.sync_blocks([self.nodes[0], self.nodes[2]]) conflicted = self.nodes[0].gettransaction(conflicted_txid) conflicting = self.nodes[0].gettransaction(conflicting_txid) @@ -89,7 +87,7 @@ class ReorgsRestoreTest(BitcoinTestFramework): # Node0 wallet file is loaded on longest sync'ed node1 self.stop_node(1) self.nodes[0].backupwallet(os.path.join(self.nodes[0].datadir, 'wallet.bak')) - shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallet.dat')) + shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, self.default_wallet_name, self.wallet_data_filename)) self.start_node(1) tx_after_reorg = self.nodes[1].gettransaction(txid) # Check that normal confirmed tx is confirmed again but with different blockhash diff --git a/test/functional/wallet_resendwallettransactions.py b/test/functional/wallet_resendwallettransactions.py index d3c03c4764..78d88f8aa5 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -11,6 +11,7 @@ from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal + class ResendWalletTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -27,10 +28,10 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): txid = node.sendtoaddress(node.getnewaddress(), 1) # Wallet rebroadcast is first scheduled 1 sec after startup (see - # nNextResend in ResendWalletTransactions()). Sleep for just over a - # second to be certain that it has been called before the first + # nNextResend in ResendWalletTransactions()). Tell scheduler to call + # MaybeResendWalletTxn now to initialize nNextResend before the first # setmocktime call below. - time.sleep(1.1) + node.mockscheduler(1) # Can take a few seconds due to transaction trickling peer_first.wait_for_broadcast([txid]) @@ -49,6 +50,7 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): block.solve() node.submitblock(ToHex(block)) + # Set correct m_best_block_time, which is used in ResendWalletTransactions node.syncwithvalidationinterfacequeue() now = int(time.time()) @@ -57,13 +59,18 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): twelve_hrs = 12 * 60 * 60 two_min = 2 * 60 node.setmocktime(now + twelve_hrs - two_min) - time.sleep(2) # ensure enough time has passed for rebroadcast attempt to occur + node.mockscheduler(1) # Tell scheduler to call MaybeResendWalletTxn now assert_equal(int(txid, 16) in peer_second.get_invs(), False) self.log.info("Bump time & check that transaction is rebroadcast") # Transaction should be rebroadcast approximately 24 hours in the future, # but can range from 12-36. So bump 36 hours to be sure. - node.setmocktime(now + 36 * 60 * 60) + with node.assert_debug_log(['ResendWalletTransactions: resubmit 1 unconfirmed transactions']): + node.setmocktime(now + 36 * 60 * 60) + # Tell scheduler to call MaybeResendWalletTxn now. + node.mockscheduler(1) + # Give some time for trickle to occur + node.setmocktime(now + 36 * 60 * 60 + 600) peer_second.wait_for_broadcast([txid]) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index b64d2030a4..192e9065e6 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -5,13 +5,15 @@ """Test the send RPC command.""" from decimal import Decimal, getcontext +from itertools import product + from test_framework.authproxy import JSONRPCException from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_fee_amount, assert_greater_than, - assert_raises_rpc_error + assert_raises_rpc_error, ) class WalletSendTest(BitcoinTestFramework): @@ -28,10 +30,10 @@ class WalletSendTest(BitcoinTestFramework): self.skip_if_no_wallet() def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, - arg_conf_target=None, arg_estimate_mode=None, - conf_target=None, estimate_mode=None, add_to_wallet=None,psbt=None, - inputs=None,add_inputs=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, + arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None, + conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, + inputs=None, add_inputs=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): assert (amount is None) != (data is None) @@ -62,6 +64,8 @@ class WalletSendTest(BitcoinTestFramework): options["conf_target"] = conf_target if estimate_mode is not None: options["estimate_mode"] = estimate_mode + if fee_rate is not None: + options["fee_rate"] = fee_rate if inputs is not None: options["inputs"] = inputs if add_inputs is not None: @@ -89,16 +93,19 @@ class WalletSendTest(BitcoinTestFramework): options = None if expect_error is None: - res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options) + res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options) else: try: - assert_raises_rpc_error(expect_error[0],expect_error[1],from_wallet.send, - outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options) + assert_raises_rpc_error(expect_error[0], expect_error[1], from_wallet.send, + outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options) except AssertionError: # Provide debug info if the test fails self.log.error("Unexpected successful result:") + self.log.error(arg_conf_target) + self.log.error(arg_estimate_mode) + self.log.error(arg_fee_rate) self.log.error(options) - res = from_wallet.send(outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options) + res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options) self.log.error(res) if "txid" in res and add_to_wallet: self.log.error("Transaction details:") @@ -131,7 +138,7 @@ class WalletSendTest(BitcoinTestFramework): assert tx assert_equal(tx["bip125-replaceable"], "yes" if replaceable else "no") # Ensure transaction exists in the mempool: - tx = from_wallet.getrawtransaction(res["txid"],True) + tx = from_wallet.getrawtransaction(res["txid"], True) assert tx if amount: if subtract_fee_from_outputs: @@ -156,7 +163,7 @@ class WalletSendTest(BitcoinTestFramework): def run_test(self): self.log.info("Setup wallets...") # w0 is a wallet with coinbase rewards - w0 = self.nodes[0].get_wallet_rpc("") + w0 = self.nodes[0].get_wallet_rpc(self.default_wallet_name) # w1 is a regular wallet self.nodes[1].createwallet(wallet_name="w1") w1 = self.nodes[1].get_wallet_rpc("w1") @@ -164,7 +171,7 @@ class WalletSendTest(BitcoinTestFramework): self.nodes[1].createwallet(wallet_name="w2") w2 = self.nodes[1].get_wallet_rpc("w2") # w3 is a watch-only wallet, based on w2 - self.nodes[1].createwallet(wallet_name="w3",disable_private_keys=True) + self.nodes[1].createwallet(wallet_name="w3", disable_private_keys=True) w3 = self.nodes[1].get_wallet_rpc("w3") for _ in range(3): a2_receive = w2.getnewaddress() @@ -188,7 +195,7 @@ class WalletSendTest(BitcoinTestFramework): self.sync_blocks() # w4 has private keys enabled, but only contains watch-only keys (from w2) - self.nodes[1].createwallet(wallet_name="w4",disable_private_keys=False) + self.nodes[1].createwallet(wallet_name="w4", disable_private_keys=False) w4 = self.nodes[1].get_wallet_rpc("w4") for _ in range(3): a2_receive = w2.getnewaddress() @@ -224,8 +231,10 @@ class WalletSendTest(BitcoinTestFramework): assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) # but not at the same time - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", - conf_target=1, estimate_mode="economical", add_to_wallet=False, expect_error=(-8,"Use either conf_target and estimate_mode or the options dictionary to control fee rate")) + for mode in ["unset", "economical", "conservative"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", + conf_target=1, estimate_mode=mode, add_to_wallet=False, + expect_error=(-8, "Pass conf_target and estimate_mode either as arguments or in the options object, but not both")) self.log.info("Create PSBT from watch-only wallet w3, sign with w2...") res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1) @@ -246,19 +255,63 @@ class WalletSendTest(BitcoinTestFramework): res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] - self.log.info("Set fee rate...") - res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=2, estimate_mode="sat/b", add_to_wallet=False) + self.log.info("Test setting explicit fee rate") + res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, add_to_wallet=False) + res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=1, add_to_wallet=False) + assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=7, add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007")) + + # "unset" and None are treated the same for estimate_mode + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=2, estimate_mode="unset", add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b", - expect_error=(-3, "Amount out of range")) - # Fee rate of 0.1 satoshi per byte should throw an error - # TODO: error should say 1.000 sat/b - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.000001, estimate_mode="BTC/KB", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=4.531, add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531")) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=3, add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003")) + + # Test that passing fee_rate as both an argument and an option raises. + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, fee_rate=1, add_to_wallet=False, + expect_error=(-8, "Pass the fee_rate either as an argument, or in the options object, but not both")) + + assert_raises_rpc_error(-8, "Use fee_rate (sat/vB) instead of feeRate", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"feeRate": 0.01}) + + assert_raises_rpc_error(-3, "Unexpected key totalFee", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"totalFee": 0.01}) + + for target, mode in product([-1, 0, 1009], ["economical", "conservative"]): + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode, + expect_error=(-8, "Invalid conf_target, must be between 1 and 1008")) # max value of 1008 per src/policy/fees.h + msg = 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"' + for target, mode in product([-1, 0], ["btc/kb", "sat/b"]): + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode, expect_error=(-8, msg)) + for mode in ["", "foo", Decimal("3.141592")]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode=mode, expect_error=(-8, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode, expect_error=(-8, msg)) + assert_raises_rpc_error(-8, msg, w0.send, {w1.getnewaddress(): 1}, 0.1, mode) + + for mode in ["economical", "conservative", "btc/kb", "sat/b"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "true", "object": {"foo": "bar"}}.items(): + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode, + expect_error=(-3, "Expected type number for conf_target, got {}".format(k))) + + # Test setting explicit fee rate just below the minimum and at zero. + self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if fee_rate of 0.99999999 is passed") + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0.99999999, + expect_error=(-4, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0.99999999, + expect_error=(-4, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0, + expect_error=(-4, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0, + expect_error=(-4, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", add_to_wallet=False) @@ -316,6 +369,7 @@ class WalletSendTest(BitcoinTestFramework): res = self.nodes[0].sendrawtransaction(hex) self.nodes[0].generate(1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 1) + self.sync_all() self.log.info("Lock unspents...") utxo1 = w0.listunspent()[0] @@ -325,11 +379,16 @@ class WalletSendTest(BitcoinTestFramework): locked_coins = w0.listlockunspent() assert_equal(len(locked_coins), 1) # Locked coins are automatically unlocked when manually selected - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1],add_to_wallet=False) + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1], add_to_wallet=False) + assert res["complete"] self.log.info("Replaceable...") - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=True) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=False) + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=True) + assert res["complete"] + assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "yes") + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=False) + assert res["complete"] + assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "no") self.log.info("Subtract fee from output") self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0]) diff --git a/test/functional/wallet_txn_clone.py b/test/functional/wallet_txn_clone.py index 33a2a9411e..bdbbb3e530 100755 --- a/test/functional/wallet_txn_clone.py +++ b/test/functional/wallet_txn_clone.py @@ -8,8 +8,6 @@ import io from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, ) from test_framework.messages import CTransaction, COIN @@ -30,7 +28,7 @@ class TxnMallTest(BitcoinTestFramework): def setup_network(self): # Start with split network: super().setup_network() - disconnect_nodes(self.nodes[1], 2) + self.disconnect_nodes(1, 2) def run_test(self): if self.options.segwit: @@ -118,7 +116,7 @@ class TxnMallTest(BitcoinTestFramework): self.nodes[2].generate(1) # Reconnect the split network, and sync chain: - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) self.nodes[2].sendrawtransaction(node0_tx2["hex"]) self.nodes[2].sendrawtransaction(tx2["hex"]) self.nodes[2].generate(1) # Mine another block to make sure we sync diff --git a/test/functional/wallet_txn_doublespend.py b/test/functional/wallet_txn_doublespend.py index cac58aeaf2..42de131354 100755 --- a/test/functional/wallet_txn_doublespend.py +++ b/test/functional/wallet_txn_doublespend.py @@ -8,8 +8,6 @@ from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - connect_nodes, - disconnect_nodes, find_output, ) @@ -28,7 +26,7 @@ class TxnMallTest(BitcoinTestFramework): def setup_network(self): # Start with split network: super().setup_network() - disconnect_nodes(self.nodes[1], 2) + self.disconnect_nodes(1, 2) def run_test(self): # All nodes should start with 1,250 BTC: @@ -116,7 +114,7 @@ class TxnMallTest(BitcoinTestFramework): self.nodes[2].generate(1) # Reconnect the split network, and sync chain: - connect_nodes(self.nodes[1], 2) + self.connect_nodes(1, 2) self.nodes[2].generate(1) # Mine another block to make sure we sync self.sync_blocks() assert_equal(self.nodes[0].gettransaction(doublespend_txid)["confirmations"], 2) diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index 031da8da81..d0bb6135a8 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -13,24 +13,47 @@ Only v0.15.2 and v0.16.3 are required by this test. The others are used in featu import os import shutil +import struct +from io import BytesIO + +from test_framework.bdb import dump_bdb_kv +from test_framework.messages import deser_compact_size, deser_string from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - assert_greater_than, assert_is_hex_string, + sha256sum_file, ) +UPGRADED_KEYMETA_VERSION = 12 + +def deser_keymeta(f): + ver, create_time = struct.unpack('<Iq', f.read(12)) + kp_str = deser_string(f) + seed_id = f.read(20) + fpr = f.read(4) + path_len = 0 + path = [] + has_key_orig = False + if ver == UPGRADED_KEYMETA_VERSION: + path_len = deser_compact_size(f) + for i in range(0, path_len): + path.append(struct.unpack('<I', f.read(4))[0]) + has_key_orig = bool(f.read(1)) + return ver, create_time, kp_str, seed_id, fpr, path_len, path, has_key_orig + class UpgradeWalletTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.extra_args = [ - ["-addresstype=bech32", "-wallet="], # current wallet version - ["-usehd=1"], # v0.16.3 wallet - ["-usehd=0"] # v0.15.2 wallet + ["-addresstype=bech32", "-keypool=2"], # current wallet version + ["-usehd=1", "-keypool=2"], # v0.16.3 wallet + ["-usehd=0", "-keypool=2"] # v0.15.2 wallet ] + self.wallet_names = [self.default_wallet_name, None, None] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -46,6 +69,7 @@ class UpgradeWalletTest(BitcoinTestFramework): 150200, ]) self.start_nodes() + self.import_deterministic_coinbase_privkeys() def dumb_sync_blocks(self): """ @@ -66,6 +90,32 @@ class UpgradeWalletTest(BitcoinTestFramework): v16_3_node.submitblock(b) assert_equal(v16_3_node.getblockcount(), to_height) + def test_upgradewallet(self, wallet, previous_version, requested_version=None, expected_version=None): + unchanged = expected_version == previous_version + new_version = previous_version if unchanged else expected_version if expected_version else requested_version + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + assert_equal(wallet.upgradewallet(requested_version), + { + "wallet_name": "", + "previous_version": previous_version, + "current_version": new_version, + "result": "Already at latest version. Wallet version unchanged." if unchanged else "Wallet upgraded successfully from version {} to version {}.".format(previous_version, new_version), + } + ) + assert_equal(wallet.getwalletinfo()["walletversion"], new_version) + + def test_upgradewallet_error(self, wallet, previous_version, requested_version, msg): + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + assert_equal(wallet.upgradewallet(requested_version), + { + "wallet_name": "", + "previous_version": previous_version, + "current_version": previous_version, + "error": msg, + } + ) + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + def run_test(self): self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress()) self.dumb_sync_blocks() @@ -85,55 +135,222 @@ class UpgradeWalletTest(BitcoinTestFramework): self.log.info("Test upgradewallet RPC...") # Prepare for copying of the older wallet - node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets") + node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets", self.default_wallet_name) + node_master_wallet = os.path.join(node_master_wallet_dir, self.default_wallet_name, self.wallet_data_filename) v16_3_wallet = os.path.join(v16_3_node.datadir, "regtest/wallets/wallet.dat") v15_2_wallet = os.path.join(v15_2_node.datadir, "regtest/wallet.dat") + split_hd_wallet = os.path.join(v15_2_node.datadir, "regtest/splithd") self.stop_nodes() - # Copy the 0.16.3 wallet to the last Bitcoin Core version and open it: - shutil.rmtree(node_master_wallet_dir) - os.mkdir(node_master_wallet_dir) - shutil.copy( - v16_3_wallet, - node_master_wallet_dir - ) - self.restart_node(0, ['-nowallet']) - node_master.loadwallet('') + # Make split hd wallet + self.start_node(2, ['-usehd=1', '-keypool=2', '-wallet=splithd']) + self.stop_node(2) + + def copy_v16(): + node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet() + # Copy the 0.16.3 wallet to the last Bitcoin Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v16_3_wallet, + node_master_wallet_dir + ) + node_master.loadwallet(self.default_wallet_name) + + def copy_non_hd(): + node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet() + # Copy the 0.15.2 non hd wallet to the last Bitcoin Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v15_2_wallet, + node_master_wallet_dir + ) + node_master.loadwallet(self.default_wallet_name) - wallet = node_master.get_wallet_rpc('') - old_version = wallet.getwalletinfo()["walletversion"] + def copy_split_hd(): + node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet() + # Copy the 0.15.2 split hd wallet to the last Bitcoin Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + split_hd_wallet, + os.path.join(node_master_wallet_dir, 'wallet.dat') + ) + node_master.loadwallet(self.default_wallet_name) - # calling upgradewallet without version arguments - # should return nothing if successful - assert_equal(wallet.upgradewallet(), "") - new_version = wallet.getwalletinfo()["walletversion"] - # upgraded wallet version should be greater than older one - assert_greater_than(new_version, old_version) + self.restart_node(0) + copy_v16() + wallet = node_master.get_wallet_rpc(self.default_wallet_name) + self.log.info("Test upgradewallet without a version argument") + self.test_upgradewallet(wallet, previous_version=159900, expected_version=169900) # wallet should still contain the same balance assert_equal(wallet.getbalance(), v16_3_balance) - self.stop_node(0) - # Copy the 0.15.2 wallet to the last Bitcoin Core version and open it: - shutil.rmtree(node_master_wallet_dir) - os.mkdir(node_master_wallet_dir) - shutil.copy( - v15_2_wallet, - node_master_wallet_dir - ) - self.restart_node(0, ['-nowallet']) - node_master.loadwallet('') - - wallet = node_master.get_wallet_rpc('') + copy_non_hd() + wallet = node_master.get_wallet_rpc(self.default_wallet_name) # should have no master key hash before conversion assert_equal('hdseedid' in wallet.getwalletinfo(), False) - # calling upgradewallet with explicit version number - # should return nothing if successful - assert_equal(wallet.upgradewallet(169900), "") - new_version = wallet.getwalletinfo()["walletversion"] - # upgraded wallet should have version 169900 - assert_equal(new_version, 169900) + self.log.info("Test upgradewallet with explicit version number") + self.test_upgradewallet(wallet, previous_version=60000, requested_version=169900) # after conversion master key hash should be present assert_is_hex_string(wallet.getwalletinfo()['hdseedid']) + self.log.info("Intermediary versions don't effect anything") + copy_non_hd() + # Wallet starts with 60000 + assert_equal(60000, wallet.getwalletinfo()['walletversion']) + wallet.unloadwallet() + before_checksum = sha256sum_file(node_master_wallet) + node_master.loadwallet('') + # Test an "upgrade" from 60000 to 129999 has no effect, as the next version is 130000 + self.test_upgradewallet(wallet, previous_version=60000, requested_version=129999, expected_version=60000) + wallet.unloadwallet() + assert_equal(before_checksum, sha256sum_file(node_master_wallet)) + node_master.loadwallet('') + + self.log.info('Wallets cannot be downgraded') + copy_non_hd() + self.test_upgradewallet_error(wallet, previous_version=60000, requested_version=40000, + msg="Cannot downgrade wallet from version 60000 to version 40000. Wallet version unchanged.") + wallet.unloadwallet() + assert_equal(before_checksum, sha256sum_file(node_master_wallet)) + node_master.loadwallet('') + + self.log.info('Can upgrade to HD') + # Inspect the old wallet and make sure there is no hdchain + orig_kvs = dump_bdb_kv(node_master_wallet) + assert b'\x07hdchain' not in orig_kvs + # Upgrade to HD, no split + self.test_upgradewallet(wallet, previous_version=60000, requested_version=130000) + # Check that there is now a hd chain and it is version 1, no internal chain counter + new_kvs = dump_bdb_kv(node_master_wallet) + assert b'\x07hdchain' in new_kvs + hd_chain = new_kvs[b'\x07hdchain'] + assert_equal(28, len(hd_chain)) + hd_chain_version, external_counter, seed_id = struct.unpack('<iI20s', hd_chain) + 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 + 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 + info = wallet.getaddressinfo(wallet.getrawchangeaddress()) + assert_equal(prev_seed_id, info['hdseedid']) + assert_equal('m/0\'/0\'/1\'', info['hdkeypath']) + + self.log.info('Cannot upgrade to HD Split, needs Pre Split Keypool') + for version in [139900, 159900, 169899]: + self.test_upgradewallet_error(wallet, previous_version=130000, requested_version=version, + msg="Cannot upgrade a non HD split wallet from version {} to version {} without upgrading to " + "support pre-split keypool. Please use version 169900 or no version specified.".format(130000, version)) + + self.log.info('Upgrade HD to HD chain split') + self.test_upgradewallet(wallet, previous_version=130000, requested_version=169900) + # Check that the hdchain updated correctly + new_kvs = dump_bdb_kv(node_master_wallet) + hd_chain = new_kvs[b'\x07hdchain'] + assert_equal(32, len(hd_chain)) + hd_chain_version, external_counter, seed_id, internal_counter = struct.unpack('<iI20sI', hd_chain) + assert_equal(2, hd_chain_version) + assert_equal(0, internal_counter) + seed_id = bytearray(seed_id) + seed_id.reverse() + assert_equal(seed_id.hex(), prev_seed_id) + # Next change address is the same keypool + info = wallet.getaddressinfo(wallet.getrawchangeaddress()) + assert_equal(prev_seed_id, info['hdseedid']) + assert_equal('m/0\'/0\'/2\'', info['hdkeypath']) + # Next change address is the new keypool + info = wallet.getaddressinfo(wallet.getrawchangeaddress()) + assert_equal(prev_seed_id, info['hdseedid']) + assert_equal('m/0\'/1\'/0\'', info['hdkeypath']) + # External addresses use the same keypool + info = wallet.getaddressinfo(wallet.getnewaddress()) + assert_equal(prev_seed_id, info['hdseedid']) + assert_equal('m/0\'/0\'/3\'', info['hdkeypath']) + + self.log.info('Upgrade non-HD to HD chain split') + copy_non_hd() + self.test_upgradewallet(wallet, previous_version=60000, requested_version=169900) + # Check that the hdchain updated correctly + new_kvs = dump_bdb_kv(node_master_wallet) + hd_chain = new_kvs[b'\x07hdchain'] + assert_equal(32, len(hd_chain)) + 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 + info = wallet.getaddressinfo(wallet.getnewaddress()) + ext_id = info['hdseedid'] + assert_equal('m/0\'/0\'/0\'', info['hdkeypath']) + info = wallet.getaddressinfo(wallet.getrawchangeaddress()) + assert_equal(ext_id, info['hdseedid']) + assert_equal('m/0\'/1\'/0\'', info['hdkeypath']) + + self.log.info('KeyMetadata should upgrade when loading into master') + copy_v16() + old_kvs = dump_bdb_kv(v16_3_wallet) + new_kvs = dump_bdb_kv(node_master_wallet) + for k, old_v in old_kvs.items(): + if k.startswith(b'\x07keymeta'): + new_ver, new_create_time, new_kp_str, new_seed_id, new_fpr, new_path_len, new_path, new_has_key_orig = deser_keymeta(BytesIO(new_kvs[k])) + old_ver, old_create_time, old_kp_str, old_seed_id, old_fpr, old_path_len, old_path, old_has_key_orig = deser_keymeta(BytesIO(old_v)) + assert_equal(10, old_ver) + if old_kp_str == b"": # imported things that don't have keymeta (i.e. imported coinbase privkeys) won't be upgraded + assert_equal(new_kvs[k], old_v) + continue + assert_equal(12, new_ver) + assert_equal(new_create_time, old_create_time) + assert_equal(new_kp_str, old_kp_str) + assert_equal(new_seed_id, old_seed_id) + assert_equal(0, old_path_len) + assert_equal(new_path_len, len(new_path)) + assert_equal([], old_path) + assert_equal(False, old_has_key_orig) + assert_equal(True, new_has_key_orig) + + # Check that the path is right + built_path = [] + for s in new_kp_str.decode().split('/')[1:]: + h = 0 + if s[-1] == '\'': + s = s[:-1] + h = 0x80000000 + p = int(s) | h + built_path.append(p) + assert_equal(new_path, built_path) + + self.log.info('Upgrading to NO_DEFAULT_KEY should not remove the defaultkey') + copy_split_hd() + # Check the wallet has a default key initially + old_kvs = dump_bdb_kv(node_master_wallet) + defaultkey = old_kvs[b'\x0adefaultkey'] + self.log.info("Upgrade the wallet. Should still have the same default key.") + self.test_upgradewallet(wallet, previous_version=139900, requested_version=159900) + new_kvs = dump_bdb_kv(node_master_wallet) + up_defaultkey = new_kvs[b'\x0adefaultkey'] + assert_equal(defaultkey, up_defaultkey) + # 0.16.3 doesn't have a default key + v16_3_kvs = dump_bdb_kv(v16_3_wallet) + assert b'\x0adefaultkey' not in v16_3_kvs + + if __name__ == '__main__': UpgradeWalletTest().main() |