diff --git a/test/functional/abc-parkedchain.py b/test/functional/abc-parkedchain.py index 2412d26ba..5a6da3bd2 100755 --- a/test/functional/abc-parkedchain.py +++ b/test/functional/abc-parkedchain.py @@ -1,252 +1,237 @@ #!/usr/bin/env python3 # Copyright (c) 2018 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the parckblock and unparkblock RPC calls.""" from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class ParkedChainTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-noparkdeepreorg", "-noautomaticunparking", "-whitelist=noban@127.0.0.1"], ["-maxreorgdepth=-1"]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() # There should only be one chaintip, which is expected_tip def only_valid_tip(self, expected_tip, other_tip_status=None): node = self.nodes[0] assert_equal(node.getbestblockhash(), expected_tip) for tip in node.getchaintips(): if tip["hash"] == expected_tip: assert_equal(tip["status"], "active") else: assert_equal(tip["status"], other_tip_status) def run_test(self): def wait_for_tip(node, tip): def check_tip(): return node.getbestblockhash() == tip self.wait_until(check_tip) node = self.nodes[0] parking_node = self.nodes[1] self.log.info("Test chain parking...") self.generate(node, 10, sync_fun=self.no_op) tip = node.getbestblockhash() self.generate(node, 1, sync_fun=self.no_op) block_to_park = node.getbestblockhash() - self.generate(node, 10, sync_fun=self.no_op) + self.generate(node, 10) parked_tip = node.getbestblockhash() - # Get parking_node caught up. - wait_for_tip(parking_node, parked_tip) - # Let's park the chain. assert parked_tip != tip assert block_to_park != tip assert block_to_park != parked_tip node.parkblock(block_to_park) assert_equal(node.getbestblockhash(), tip) # When the chain is unparked, the node reorg into its original chain. node.unparkblock(parked_tip) assert_equal(node.getbestblockhash(), parked_tip) # Parking and then unparking a block should not change its validity, # and invaliding and reconsidering a block should not change its # parked state. See the following test cases: self.log.info("Test invalidate, park, unpark, reconsider...") self.generate(node, 1, sync_fun=self.no_op) tip = node.getbestblockhash() self.generate(node, 1, sync_fun=self.no_op) bad_tip = node.getbestblockhash() # Generate an extra block to check that children are invalidated as # expected and do not produce dangling chaintips - self.generate(node, 1, sync_fun=self.no_op) + self.generate(node, 1) good_tip = node.getbestblockhash() - # avoid race condition from parking_node requesting block when invalid - wait_for_tip(parking_node, good_tip) - node.invalidateblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.parkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.unparkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.reconsiderblock(bad_tip) self.only_valid_tip(good_tip) self.log.info("Test park, invalidate, reconsider, unpark") self.generate(node, 1, sync_fun=self.no_op) tip = node.getbestblockhash() self.generate(node, 1, sync_fun=self.no_op) bad_tip = node.getbestblockhash() - self.generate(node, 1, sync_fun=self.no_op) + self.generate(node, 1) good_tip = node.getbestblockhash() - # avoid race condition from parking_node requesting block when invalid - wait_for_tip(parking_node, good_tip) - node.parkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="parked") node.invalidateblock(bad_tip) # NOTE: Intuitively, other_tip_status would be "invalid", but because # only valid (unparked) chains are walked, child blocks' statuses are # not updated, so the "parked" state remains. self.only_valid_tip(tip, other_tip_status="parked") node.reconsiderblock(bad_tip) self.only_valid_tip(tip, other_tip_status="parked") node.unparkblock(bad_tip) self.only_valid_tip(good_tip) self.log.info("Test invalidate, park, reconsider, unpark...") self.generate(node, 1, sync_fun=self.no_op) tip = node.getbestblockhash() self.generate(node, 1, sync_fun=self.no_op) bad_tip = node.getbestblockhash() - self.generate(node, 1, sync_fun=self.no_op) + self.generate(node, 1) good_tip = node.getbestblockhash() - # avoid race condition from parking_node requesting block when invalid - wait_for_tip(parking_node, good_tip) - node.invalidateblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.parkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.reconsiderblock(bad_tip) self.only_valid_tip(tip, other_tip_status="parked") node.unparkblock(bad_tip) self.only_valid_tip(good_tip) self.log.info("Test park, invalidate, unpark, reconsider") self.generate(node, 1, sync_fun=self.no_op) tip = node.getbestblockhash() self.generate(node, 1, sync_fun=self.no_op) bad_tip = node.getbestblockhash() - self.generate(node, 1, sync_fun=self.no_op) + self.generate(node, 1) good_tip = node.getbestblockhash() - # avoid race condition from parking_node requesting block when invalid - wait_for_tip(parking_node, good_tip) - node.parkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="parked") node.invalidateblock(bad_tip) # NOTE: Intuitively, other_tip_status would be "invalid", but because # only valid (unparked) chains are walked, child blocks' statuses are # not updated, so the "parked" state remains. self.only_valid_tip(tip, other_tip_status="parked") node.unparkblock(bad_tip) self.only_valid_tip(tip, other_tip_status="invalid") node.reconsiderblock(bad_tip) self.only_valid_tip(good_tip) # To get ready for next testset, make sure both nodes are in sync. wait_for_tip(parking_node, good_tip) assert_equal(node.getbestblockhash(), parking_node.getbestblockhash()) # Wait for node 1 to park the chain. def wait_for_parked_block(block): def check_block(): for tip in parking_node.getchaintips(): if tip["hash"] == block: assert tip["status"] != "active" return tip["status"] == "parked" return False self.wait_until(check_block) def check_reorg_protection(depth, extra_blocks): self.log.info( "Test deep reorg parking, {} block deep".format(depth)) # Invalidate the tip on node 0, so it doesn't follow node 1. node.invalidateblock(node.getbestblockhash()) # Mine block to create a fork of proper depth self.generatetoaddress(parking_node, nblocks=depth - 1, address=parking_node.getnewaddress( label='coinbase'), sync_fun=self.no_op, ) self.generatetoaddress(node, nblocks=depth, address=node.getnewaddress( label='coinbase'), sync_fun=self.no_op, ) # extra block should now find themselves parked for _ in range(extra_blocks): self.generate(node, 1, sync_fun=self.no_op) wait_for_parked_block(node.getbestblockhash()) # If we mine one more block, the node reorgs (generate also waits # for chain sync). self.generate(node, 1) check_reorg_protection(1, 0) check_reorg_protection(2, 0) check_reorg_protection(3, 1) check_reorg_protection(4, 4) check_reorg_protection(5, 5) check_reorg_protection(6, 6) check_reorg_protection(100, 100) # try deep reorg with a log check. with parking_node.assert_debug_log(["Park block"]): check_reorg_protection(3, 1) self.log.info( "Accepting many blocks at once (possibly out of order) should not park if there is no reorg.") # rewind one block to make a reorg that is shallow. node.invalidateblock(parking_node.getbestblockhash()) # generate a ton of blocks at once. try: with parking_node.assert_debug_log(["Park block"]): # Also waits for chain sync self.generatetoaddress(node, nblocks=20, address=node.getnewaddress(label='coinbase')) except AssertionError as exc: # good, we want an absence of "Park block" messages assert "does not partially match log" in exc.args[0] else: raise AssertionError("Parked block when there was no deep reorg") self.log.info("Test that unparking works when -parkdeepreorg=0") # Set up parking node height = fork + 4, node height = fork + 5 node.invalidateblock(node.getbestblockhash()) self.generate(parking_node, 3, sync_fun=self.no_op) self.generatetoaddress(node, nblocks=5, address=node.getnewaddress(label='coinbase'), sync_fun=self.no_op, ) wait_for_parked_block(node.getbestblockhash()) # Restart the parking node without parkdeepreorg. self.restart_node(1, ["-parkdeepreorg=0"]) parking_node = self.nodes[1] self.connect_nodes(node.index, parking_node.index) # The other chain should still be marked 'parked'. wait_for_parked_block(node.getbestblockhash()) # Three more blocks is not enough to unpark. Even though its PoW is # larger, we are still following the delayed-unparking rules. self.generate(node, 3, sync_fun=self.no_op) wait_for_parked_block(node.getbestblockhash()) # Final block pushes over the edge, and should unpark (generate also # waits for chain sync). self.generate(node, 1) # Do not append tests after this point without restarting node again. # Parking node is no longer parking. if __name__ == '__main__': ParkedChainTest().main() diff --git a/test/functional/abc_p2p_avalanche_peer_discovery.py b/test/functional/abc_p2p_avalanche_peer_discovery.py index df5e893f7..66d3a4a20 100755 --- a/test/functional/abc_p2p_avalanche_peer_discovery.py +++ b/test/functional/abc_p2p_avalanche_peer_discovery.py @@ -1,323 +1,322 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the peer discovery behavior of avalanche nodes. This includes tests for the service flag, avahello handshake and proof exchange. """ import time from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( AvaP2PInterface, avalanche_proof_from_hex, create_coinbase_stakes, gen_proof, get_ava_p2p_interface_no_handshake, get_proof_ids, wait_for_proof, ) from test_framework.key import ECKey, ECPubKey from test_framework.messages import ( MSG_AVA_PROOF, MSG_TYPE_MASK, NODE_AVALANCHE, NODE_NETWORK, CInv, msg_getdata, ) from test_framework.p2p import p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, uint256_hex from test_framework.wallet_util import bytes_to_wif UNCONDITIONAL_RELAY_DELAY = 2 * 60 class GetProofDataCountingInterface(AvaP2PInterface): def __init__(self): self.get_proof_data_count = 0 super().__init__() def on_getdata(self, message): if message.inv.type & MSG_TYPE_MASK == MSG_AVA_PROOF: self.get_proof_data_count += 1 class AvalanchePeerDiscoveryTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-avalanche=1', '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=3']] self.supports_cli = False def run_test(self): node = self.nodes[0] # duplicate the deterministic sig test from src/test/key_tests.cpp privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) wif_privkey = bytes_to_wif(privkey.get_bytes()) self.log.info( "Check the node is signalling the avalanche service bit only if there is a proof.") assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, 0) # Create stakes by mining blocks addrkey0 = node.get_deterministic_priv_key() blockhashes = self.generatetoaddress( node, 4, addrkey0.address, sync_fun=self.no_op) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof_sequence = 11 proof_expiration = 0 proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) self.log.info("Test the avahello signature with no proof") peer = get_ava_p2p_interface_no_handshake(node) avahello = peer.wait_for_avahello().hello assert_equal(avahello.delegation.limited_proofid, 0) self.log.info( "A delegation with all zero limited id indicates that the peer has no proof") no_proof_peer = GetProofDataCountingInterface() node.add_p2p_connection(no_proof_peer, wait_for_verack=True) no_proof_peer.sync_send_with_ping() # No proof is requested with p2p_lock: assert_equal(no_proof_peer.get_proof_data_count, 0) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, NODE_AVALANCHE) def check_avahello(args): # Restart the node with the given args self.restart_node(0, self.extra_args[0] + args) peer = get_ava_p2p_interface_no_handshake(node) avahello = peer.wait_for_avahello().hello avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(peer)) self.log.info( "Test the avahello signature with a generated delegation") check_avahello([ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN" ]) master_key = ECKey() master_key.generate() limited_id = avalanche_proof_from_hex(proof).limited_proofid delegation = node.delegateavalancheproof( uint256_hex(limited_id), bytes_to_wif(privkey.get_bytes()), master_key.get_pubkey().get_bytes().hex(), ) self.log.info("Test the avahello signature with a supplied delegation") check_avahello([ "-avaproof={}".format(proof), "-avadelegation={}".format(delegation), "-avamasterkey={}".format(bytes_to_wif(master_key.get_bytes())), ]) stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) interface_proof_hex = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) limited_id = avalanche_proof_from_hex( interface_proof_hex).limited_proofid # delegate delegated_key = ECKey() delegated_key.generate() interface_delegation_hex = node.delegateavalancheproof( uint256_hex(limited_id), bytes_to_wif(privkey.get_bytes()), delegated_key.get_pubkey().get_bytes().hex(), None) self.log.info("Test that wrong avahello signature causes a ban") bad_interface = get_ava_p2p_interface_no_handshake(node) wrong_key = ECKey() wrong_key.generate() with node.assert_debug_log( ["Misbehaving", "peer=1 (0 -> 100) BAN THRESHOLD EXCEEDED: invalid-avahello-signature"]): bad_interface.send_avahello(interface_delegation_hex, wrong_key) bad_interface.wait_for_disconnect() self.log.info( 'Check that receiving a valid avahello triggers a proof getdata request') good_interface = get_ava_p2p_interface_no_handshake(node) proofid = good_interface.send_avahello( interface_delegation_hex, delegated_key) def getdata_found(peer, proofid): with p2p_lock: return good_interface.last_message.get( "getdata") and good_interface.last_message["getdata"].inv[-1].hash == proofid self.wait_until(lambda: getdata_found(good_interface, proofid)) self.log.info('Check that we can download the proof from our peer') node_proofid = avalanche_proof_from_hex(proof).proofid getdata = msg_getdata([CInv(MSG_AVA_PROOF, node_proofid)]) self.log.info( "Proof has been inv'ed recently, check it can be requested") good_interface.send_message(getdata) # This is our local proof so if it was announced it can be requested # without waiting for validation. assert_equal(len(node.getavalanchepeerinfo()), 0) def proof_received(peer, proofid): with p2p_lock: return peer.last_message.get( "avaproof") and peer.last_message["avaproof"].proof.proofid == proofid self.wait_until(lambda: proof_received(good_interface, node_proofid)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) def wait_for_proof_validation(): # Connect some blocks to trigger the proof verification self.generate(node, 1, sync_fun=self.no_op) self.wait_until(lambda: node_proofid in get_proof_ids(node)) wait_for_proof_validation() self.log.info( "The proof has not been announced, it cannot be requested") peer = get_ava_p2p_interface_no_handshake(node, services=NODE_NETWORK) # Build a new proof and only announce this one _, new_proof_obj = gen_proof(self, node) new_proof = new_proof_obj.serialize().hex() new_proofid = new_proof_obj.proofid # Make the proof mature self.generate(node, 2, sync_fun=self.no_op) node.sendavalancheproof(new_proof) wait_for_proof(node, uint256_hex(new_proofid)) # Request both our local proof and the new proof getdata = msg_getdata( [CInv(MSG_AVA_PROOF, node_proofid), CInv(MSG_AVA_PROOF, new_proofid)]) peer.send_message(getdata) self.wait_until(lambda: proof_received(peer, new_proofid)) assert not proof_received(peer, node_proofid) self.log.info("The proof is known for long enough to be requested") current_time = int(time.time()) node.setmocktime(current_time + UNCONDITIONAL_RELAY_DELAY) getdata = msg_getdata([CInv(MSG_AVA_PROOF, node_proofid)]) peer.send_message(getdata) self.wait_until(lambda: proof_received(peer, node_proofid)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) wait_for_proof_validation() # The only peer is the node itself assert_equal(len(node.getavalanchepeerinfo()), 1) assert_equal(node.getavalanchepeerinfo()[0]["proof"], proof) peer = get_ava_p2p_interface_no_handshake(node) peer_proofid = peer.send_avahello( interface_delegation_hex, delegated_key) self.wait_until(lambda: getdata_found(peer, peer_proofid)) assert peer_proofid not in get_proof_ids(node) self.log.info( "Check that the peer gets added as an avalanche node as soon as the node knows about the proof") node.sendavalancheproof(interface_proof_hex) def has_node_count(count): peerinfo = node.getavalanchepeerinfo() return (len(peerinfo) == 2 and peerinfo[-1]["proof"] == interface_proof_hex and peerinfo[-1]["nodecount"] == count) self.wait_until(lambda: has_node_count(1)) self.log.info( "Check that the peer gets added immediately if the proof is already known") # Connect another peer using the same proof peer_proof_known = get_ava_p2p_interface_no_handshake(node) peer_proof_known.send_avahello(interface_delegation_hex, delegated_key) self.wait_until(lambda: has_node_count(2)) self.log.info("Check that repeated avahello messages are ignored") for i in range(3): with node.assert_debug_log(['Ignoring avahello from peer']): peer_proof_known.send_avahello( interface_delegation_hex, delegated_key) self.log.info("Invalidate the proof and check the nodes are removed") tip = node.getbestblockhash() # Invalidate the block after the proof utxo to make the proof immature node.invalidateblock(blockhashes[2]) # Change the address to make sure we don't generate a block identical # to the one we just invalidated. Can be generate(1) after D9694 or # D9697 is landed. forked_tip = self.generatetoaddress( node, 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op)[0] - self.wait_until(lambda: node.getbestblockhash() == forked_tip) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) assert peer_proofid not in get_proof_ids(node) self.log.info("Reorg back and check the nodes are added back") node.invalidateblock(forked_tip) node.reconsiderblock(tip) self.wait_until(lambda: has_node_count(2), timeout=2) self.log.info( "Check the node sends an avahello message to all peers even if the avalanche service bit is not advertised") for _ in range(3): nonavapeer = get_ava_p2p_interface_no_handshake( node, services=NODE_NETWORK) nonavapeer.wait_for_avahello() if __name__ == '__main__': AvalanchePeerDiscoveryTest().main() diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py index 6dd9b0adb..660d162a1 100755 --- a/test/functional/feature_coinstatsindex.py +++ b/test/functional/feature_coinstatsindex.py @@ -1,353 +1,350 @@ #!/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 coinstatsindex across nodes. Test that the values returned by gettxoutsetinfo are consistent between a node running the coinstatsindex and a node without the index. """ from decimal import Decimal from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import ( XEC, COutPoint, CTransaction, CTxIn, CTxOut, ToHex, ) from test_framework.script import OP_FALSE, OP_RETURN, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, try_rpc class CoinStatsIndexTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.supports_cli = False self.extra_args = [ [], ["-coinstatsindex"] ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): self._test_coin_stats_index() self._test_use_index_option() self._test_reorg_index() self._test_index_rejects_hash_serialized() def block_sanity_check(self, block_info): block_subsidy = 50_000_000 assert_equal( block_info['prevout_spent'] + block_subsidy, block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable'] ) def _test_coin_stats_index(self): node = self.nodes[0] index_node = self.nodes[1] # Both none and muhash options allow the usage of the index index_hash_options = ['none', 'muhash'] # Generate a normal transaction and mine it self.generate(node, 101) address = self.nodes[0].get_deterministic_priv_key().address node.sendtoaddress( address=address, amount=10_000_000, subtractfeefromamount=True) self.generate(node, 1) - self.sync_blocks(timeout=120) - self.log.info( "Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option") self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", node.gettxoutsetinfo)) res0 = node.gettxoutsetinfo('none') # The fields 'disk_size' and 'transactions' do not exist on the index del res0['disk_size'], res0['transactions'] self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) for hash_option in index_hash_options: res1 = index_node.gettxoutsetinfo(hash_option) # The fields 'block_info' and 'total_unspendable_amount' only exist # on the index del res1['block_info'], res1['total_unspendable_amount'] res1.pop('muhash', None) # Everything left should be the same assert_equal(res1, res0) self.log.info( "Test that gettxoutsetinfo() can get fetch data on specific " "heights with index") # Generate a new tip self.generate(node, 5) self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) for hash_option in index_hash_options: # Fetch old stats by height res2 = index_node.gettxoutsetinfo(hash_option, 102) del res2['block_info'], res2['total_unspendable_amount'] res2.pop('muhash', None) assert_equal(res0, res2) # Fetch old stats by hash res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock']) del res3['block_info'], res3['total_unspendable_amount'] res3.pop('muhash', None) assert_equal(res0, res3) # It does not work without coinstatsindex assert_raises_rpc_error( -8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102) self.log.info("Test gettxoutsetinfo() with index and verbose flag") for hash_option in index_hash_options: # Genesis block is unspendable res4 = index_node.gettxoutsetinfo(hash_option, 0) assert_equal(res4['total_unspendable_amount'], 50_000_000) assert_equal(res4['block_info'], { 'unspendable': 50_000_000, 'prevout_spent': 0, 'new_outputs_ex_coinbase': 0, 'coinbase': 0, 'unspendables': { 'genesis_block': 50_000_000, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 0 } }) self.block_sanity_check(res4['block_info']) # Test an older block height that included a normal tx res5 = index_node.gettxoutsetinfo(hash_option, 102) assert_equal(res5['total_unspendable_amount'], 50_000_000) assert_equal(res5['block_info'], { 'unspendable': 0, 'prevout_spent': 50_000_000, 'new_outputs_ex_coinbase': Decimal('49999997.75'), 'coinbase': Decimal('50000002.25'), 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 0 } }) self.block_sanity_check(res5['block_info']) # Generate and send a normal tx with two outputs tx1_inputs = [] tx1_outputs = {self.nodes[0].getnewaddress(): 21_000_000, self.nodes[0].getnewaddress(): 42_000_000} raw_tx1 = self.nodes[0].createrawtransaction(tx1_inputs, tx1_outputs) funded_tx1 = self.nodes[0].fundrawtransaction(raw_tx1) signed_tx1 = self.nodes[0].signrawtransactionwithwallet( funded_tx1['hex']) tx1_txid = self.nodes[0].sendrawtransaction(signed_tx1['hex']) # Find the right position of the 21 000 000 XEC output tx1_final = self.nodes[0].gettransaction(tx1_txid) for output in tx1_final['details']: if output['amount'] == Decimal( '21000000.00') and output['category'] == 'receive': n = output['vout'] # Generate and send another tx with an OP_RETURN output (which is # unspendable) tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(int(tx1_txid, 16), n), b'')) tx2.vout.append(CTxOut(int(20_990_000 * XEC), CScript([OP_RETURN] + [OP_FALSE] * 30))) tx2_hex = self.nodes[0].signrawtransactionwithwallet(ToHex(tx2))['hex'] self.nodes[0].sendrawtransaction(tx2_hex) # Include both txs in a block self.generate(self.nodes[0], 1) - self.sync_all() self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) for hash_option in index_hash_options: # Check all amounts were registered correctly res6 = index_node.gettxoutsetinfo(hash_option, 108) assert_equal( res6['total_unspendable_amount'], Decimal('70990000.00')) assert_equal(res6['block_info'], { 'unspendable': Decimal('20990000.00'), 'prevout_spent': 111_000_000, 'new_outputs_ex_coinbase': Decimal('89999995.94'), 'coinbase': Decimal('50010004.06'), 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': Decimal('20990000.00'), 'unclaimed_rewards': 0 } }) self.block_sanity_check(res6['block_info']) # Create a coinbase that does not claim full subsidy and also # has two outputs cb = create_coinbase(109, nValue=35_000_000) cb.vout.append(CTxOut(5_000_000 * XEC, CScript([OP_FALSE]))) cb.rehash() # Generate a block that includes previous coinbase tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblock(tip)['time'] + 1 block = create_block(int(tip, 16), cb, block_time) block.solve() self.nodes[0].submitblock(ToHex(block)) self.sync_all() self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) for hash_option in index_hash_options: res7 = index_node.gettxoutsetinfo(hash_option, 109) assert_equal( res7['total_unspendable_amount'], Decimal('80990000.00')) assert_equal(res7['block_info'], { 'unspendable': 10_000_000, 'prevout_spent': 0, 'new_outputs_ex_coinbase': 0, 'coinbase': 40_000_000, 'unspendables': { 'genesis_block': 0, 'bip30': 0, 'scripts': 0, 'unclaimed_rewards': 10_000_000 } }) self.block_sanity_check(res7['block_info']) self.log.info("Test that the index is robust across restarts") res8 = index_node.gettxoutsetinfo('muhash') self.restart_node(1, extra_args=self.extra_args[1]) res9 = index_node.gettxoutsetinfo('muhash') assert_equal(res8, res9) self.generate(index_node, 1, sync_fun=self.no_op) self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) res10 = index_node.gettxoutsetinfo('muhash') assert res8['txouts'] < res10['txouts'] def _test_use_index_option(self): self.log.info("Test use_index option for nodes running the index") self.connect_nodes(0, 1) self.nodes[0].waitforblockheight(110) res = self.nodes[0].gettxoutsetinfo('muhash') option_res = self.nodes[1].gettxoutsetinfo( hash_type='muhash', hash_or_height=None, use_index=False) del res['disk_size'], option_res['disk_size'] assert_equal(res, option_res) def _test_reorg_index(self): self.log.info("Test that index can handle reorgs") # Generate two block, let the index catch up, then invalidate the # blocks index_node = self.nodes[1] reorg_blocks = self.generatetoaddress(index_node, 2, index_node.getnewaddress()) reorg_block = reorg_blocks[1] self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) res_invalid = index_node.gettxoutsetinfo('muhash') index_node.invalidateblock(reorg_blocks[0]) assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110) # Add two new blocks block = self.generate(index_node, 2, sync_fun=self.no_op)[1] self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) res = index_node.gettxoutsetinfo( hash_type='muhash', hash_or_height=None, use_index=False) # Test that the result of the reorged block is not returned for its old # block height res2 = index_node.gettxoutsetinfo( hash_type='muhash', hash_or_height=112) assert_equal(res["bestblock"], block) assert_equal(res["muhash"], res2["muhash"]) assert res["muhash"] != res_invalid["muhash"] # Test that requesting reorged out block by hash is still returning # correct results res_invalid2 = index_node.gettxoutsetinfo( hash_type='muhash', hash_or_height=reorg_block) assert_equal(res_invalid2["muhash"], res_invalid["muhash"]) assert res["muhash"] != res_invalid2["muhash"] # Add another block, so we don't depend on reconsiderblock remembering # which blocks were touched by invalidateblock self.generate(index_node, 1) # Ensure that removing and re-adding blocks yields consistent results block = index_node.getblockhash(99) index_node.invalidateblock(block) self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) index_node.reconsiderblock(block) self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash')) res3 = index_node.gettxoutsetinfo( hash_type='muhash', hash_or_height=112) assert_equal(res2, res3) self.log.info( "Test that a node aware of stale blocks syncs them as well") node = self.nodes[0] # Ensure the node is aware of a stale block prior to restart node.getblock(reorg_block) self.restart_node(0, ["-coinstatsindex"]) self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", node.gettxoutsetinfo, 'muhash')) assert_raises_rpc_error(-32603, "Unable to read UTXO set", node.gettxoutsetinfo, 'muhash', reorg_block) def _test_index_rejects_hash_serialized(self): self.log.info( "Test that the rpc raises if the legacy hash is passed with the index") msg = "hash_serialized hash type cannot be queried for a specific block" assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized', hash_or_height=111) for use_index in {True, False, None}: assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized', hash_or_height=111, use_index=use_index) if __name__ == '__main__': CoinStatsIndexTest().main() diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 21ab884e0..e1159de0f 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -1,404 +1,396 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the REST API.""" import http.client import json import urllib.parse from decimal import Decimal from enum import Enum from io import BytesIO from struct import pack, unpack from test_framework.messages import BLOCK_HEADER_SIZE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, ) class ReqType(Enum): JSON = 1 BIN = 2 HEX = 3 class RetType(Enum): OBJ = 1 BYTES = 2 JSON = 3 def filter_output_indices_by_value(vouts, value): for vout in vouts: if vout['value'] == value: yield vout['n'] class RESTTest (BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [["-rest"], []] self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON): rest_uri = '/rest' + uri if req_type == ReqType.JSON: rest_uri += '.json' elif req_type == ReqType.BIN: rest_uri += '.bin' elif req_type == ReqType.HEX: rest_uri += '.hex' conn = http.client.HTTPConnection(self.url.hostname, self.url.port) self.log.debug('{} {} {}'.format(http_method, rest_uri, body)) if http_method == 'GET': conn.request('GET', rest_uri) elif http_method == 'POST': conn.request('POST', rest_uri, body) resp = conn.getresponse() assert_equal(resp.status, status) if ret_type == RetType.OBJ: return resp elif ret_type == RetType.BYTES: return resp.read() elif ret_type == RetType.JSON: return json.loads(resp.read().decode('utf-8'), parse_float=Decimal) def run_test(self): self.url = urllib.parse.urlparse(self.nodes[0].url) self.log.info("Mine blocks and send Bitcoin Cash to node 1") # Random address so node1's balance doesn't increase not_related_address = "2MxqoHEdNQTyYeX1mHcbrrpzgojbosTpCvJ" self.generate(self.nodes[0], 1) - self.sync_all() self.generatetoaddress(self.nodes[1], 100, not_related_address) - self.sync_all() assert_equal(self.nodes[0].getbalance(), 50000000) txid = self.nodes[0].sendtoaddress( self.nodes[1].getnewaddress(), 100000) self.sync_all() self.log.info("Test the /tx URI") json_obj = self.test_rest_request("/tx/{}".format(txid)) assert_equal(json_obj['txid'], txid) # Check hex format response hex_response = self.test_rest_request( "/tx/{}".format(txid), req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than_or_equal(int(hex_response.getheader('content-length')), json_obj['size'] * 2) # Get the vin to later check for utxo (should be spent by then) spent = (json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout']) # Get n of 0.1 outpoint n, = filter_output_indices_by_value( json_obj['vout'], Decimal('100000')) spending = (txid, n) self.log.info("Query an unspent TXO using the /getutxos URI") self.generatetoaddress(self.nodes[1], 1, not_related_address) - self.sync_all() bb_hash = self.nodes[0].getbestblockhash() assert_equal(self.nodes[1].getbalance(), Decimal("100000")) # Check chainTip response json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is one utxo assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['utxos'][0]['value'], Decimal('100000')) self.log.info("Query a spent TXO using the /getutxos URI") json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spent)) # Check chainTip response assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is no utxo in the response because this outpoint has # been spent assert_equal(len(json_obj['utxos']), 0) # Check bitmap assert_equal(json_obj['bitmap'], "0") self.log.info("Query two TXOs using the /getutxos URI") json_obj = self.test_rest_request( "/getutxos/{}-{}/{}-{}".format(*(spending + spent))) assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['bitmap'], "10") self.log.info( "Query the TXOs using the /getutxos URI with a binary response") bin_request = b'\x01\x02' for txid, n in [spending, spent]: bin_request += bytes.fromhex(txid) bin_request += pack("i", n) bin_response = self.test_rest_request( "/getutxos", http_method='POST', req_type=ReqType.BIN, body=bin_request, ret_type=RetType.BYTES) output = BytesIO(bin_response) chain_height, = unpack("C : Blockhash connected <32-byte hash>D : Blockhash disconnected <32-byte hash>R<8-byte LE uint> : Transactionhash removed from mempool for non-block inclusion reason <32-byte hash>A<8-byte LE uint> : Transactionhash added mempool """ self.log.info("Testing 'sequence' publisher") [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")]) self.disconnect_nodes(0, 1) # Mempool sequence number starts at 1 seq_num = 1 # Generate 1 block in nodes[0] and receive all notifications dc_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op)[0] # Note: We are not notified of any block transactions, coinbase or # mined assert_equal((self.nodes[0].getbestblockhash(), "C", None), seq.receive_sequence()) # Generate 2 blocks in nodes[1] to a different address to ensure # a chain split self.generatetoaddress( self.nodes[1], 2, ADDRESS_ECREG_P2SH_OP_TRUE, sync_fun=self.no_op) # nodes[0] will reorg chain after connecting back nodes[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()) block_count = self.nodes[1].getblockcount() assert_equal((self.nodes[1].getblockhash(block_count - 1), "C", None), seq.receive_sequence()) assert_equal((self.nodes[1].getblockhash(block_count), "C", None), seq.receive_sequence()) # Rest of test requires wallet functionality if self.is_wallet_compiled(): (block_hash, txid_to_be_replaced, replacement_txid ) = self.create_conflicting_tx() self.log.info( "Testing sequence notifications with mempool sequence values") # Should receive the initially broadcasted txid. assert_equal((txid_to_be_replaced, "A", seq_num), seq.receive_sequence()) seq_num += 1 self.log.info("Testing a tx removal notification") # Next we receive a notification for the transaction removal assert_equal((txid_to_be_replaced, "R", seq_num), seq.receive_sequence()) seq_num += 1 # Then we see the block notification assert_equal((block_hash, "C", None), seq.receive_sequence()) # There is no sequence notification for the transaction that was # never in node0's mempool, but it can be found in the block. assert replacement_txid in self.nodes[0].getblock(block_hash)["tx"] self.log.info("Wait for tx from second node") payment_txid = self.nodes[1].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=5_000_000) self.sync_all() assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Doesn't get published when mined, make a block and tx to "flush" # the possibility though the mempool sequence number does go up by # the number of transactions removed from the mempool by the block # mining it. mempool_size = len(self.nodes[0].getrawmempool()) c_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE)[0] - self.sync_all() # Make sure the number of mined transactions matches the number of # txs out of mempool mempool_size_delta = mempool_size - \ len(self.nodes[0].getrawmempool()) assert_equal(len(self.nodes[0].getblock(c_block)["tx"]) - 1, mempool_size_delta) seq_num += mempool_size_delta payment_txid_2 = self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), 1_000_000) self.sync_all() assert_equal((c_block, "C", None), seq.receive_sequence()) assert_equal((payment_txid_2, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Spot check getrawmempool results that they only show up when # asked for assert isinstance(self.nodes[0].getrawmempool(), list) assert isinstance( self.nodes[0].getrawmempool(mempool_sequence=False), list) assert "mempool_sequence" not in self.nodes[0].getrawmempool( verbose=True) assert_raises_rpc_error( -8, "Verbose results cannot contain mempool sequence values.", self.nodes[0].getrawmempool, True, True) assert_equal(self.nodes[0].getrawmempool( mempool_sequence=True)["mempool_sequence"], seq_num) self.log.info("Testing reorg notifications") # Manually invalidate the last block to test mempool re-entry # N.B. This part could be made more lenient in exact ordering # since it greatly depends on inner-workings of blocks/mempool # during "deep" re-orgs. Probably should "re-construct" # blockchain/mempool state from notifications instead. block_count = self.nodes[0].getblockcount() best_hash = self.nodes[0].getbestblockhash() self.nodes[0].invalidateblock(best_hash) # Bit of room to make sure transaction things happened sleep(2) # Make sure getrawmempool mempool_sequence results aren't "queued" # but immediately reflective of the time they were gathered. assert self.nodes[0].getrawmempool( mempool_sequence=True)["mempool_sequence"] > seq_num assert_equal((best_hash, "D", None), seq.receive_sequence()) assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Other things may happen but aren't wallet-deterministic so we # don't test for them currently self.nodes[0].reconsiderblock(best_hash) self.generatetoaddress(self.nodes[1], 1, ADDRESS_ECREG_UNSPENDABLE) - self.sync_all() self.log.info("Evict mempool transaction by block conflict") orig_txid = self.nodes[0].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=1_000_000) # More to be simply mined more_tx = [] for _ in range(5): more_tx.append(self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), 100_000)) raw_tx = self.nodes[0].getrawtransaction(orig_txid) block = create_block( int(self.nodes[0].getbestblockhash(), 16), create_coinbase(self.nodes[0].getblockcount() + 1)) tx = FromHex(CTransaction(), raw_tx) block.vtx.append(tx) for txid in more_tx: tx = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid)) block.vtx.append(tx) make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() block.solve() assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None) tip = self.nodes[0].getbestblockhash() assert_equal(int(tip, 16), block.sha256) orig_txid_2 = self.nodes[0].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=1_000_000) # Flush old notifications until evicted tx original entry (hash_str, label, mempool_seq) = seq.receive_sequence() while hash_str != orig_txid: (hash_str, label, mempool_seq) = seq.receive_sequence() mempool_seq += 1 # Added original tx assert_equal(label, "A") # More transactions to be simply mined for i in range(len(more_tx)): assert_equal((more_tx[i], "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 # Removed RBF tests mempool_seq += 1 assert_equal((tip, "C", None), seq.receive_sequence()) mempool_seq += len(more_tx) # Last tx assert_equal((orig_txid_2, "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) # want to make sure we didn't break "consensus" for other tests self.sync_all() def test_mempool_sync(self): """ Use sequence notification plus getrawmempool sequence results to "sync mempool" """ if not self.is_wallet_compiled(): self.log.info("Skipping mempool sync test") return self.log.info("Testing 'mempool sync' usage of sequence notifier") [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")]) # In-memory counter, should always start at 1 next_mempool_seq = self.nodes[0].getrawmempool( mempool_sequence=True)["mempool_sequence"] assert_equal(next_mempool_seq, 1) # Some transactions have been happening but we aren't consuming # zmq notifications yet or we lost a ZMQ message somehow and want # to start over txids = [] num_txs = 5 for _ in range(num_txs): txids.append(self.nodes[1].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=1_000_000)) self.sync_all() # 1) Consume backlog until we get a mempool sequence number (hash_str, label, zmq_mem_seq) = seq.receive_sequence() while zmq_mem_seq is None: (hash_str, label, zmq_mem_seq) = seq.receive_sequence() assert label == "A" assert hash_str is not None # 2) We need to "seed" our view of the mempool mempool_snapshot = self.nodes[0].getrawmempool(mempool_sequence=True) mempool_view = set(mempool_snapshot["txids"]) get_raw_seq = mempool_snapshot["mempool_sequence"] assert_equal(get_raw_seq, 6) # Snapshot may be too old compared to zmq message we read off latest while zmq_mem_seq >= get_raw_seq: sleep(2) mempool_snapshot = self.nodes[0].getrawmempool( mempool_sequence=True) mempool_view = set(mempool_snapshot["txids"]) get_raw_seq = mempool_snapshot["mempool_sequence"] # Things continue to happen in the "interim" while waiting for # snapshot results for _ in range(num_txs): txids.append(self.nodes[0].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=1_000_000)) self.sync_all() self.create_conflicting_tx() self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) final_txid = self.nodes[0].sendtoaddress( address=self.nodes[0].getnewaddress(), amount=100_000) # 3) Consume ZMQ backlog until we get to "now" for the mempool snapshot while True: if zmq_mem_seq == get_raw_seq - 1: break (hash_str, label, mempool_sequence) = seq.receive_sequence() if mempool_sequence is not None: zmq_mem_seq = mempool_sequence if zmq_mem_seq > get_raw_seq: raise Exception( f"We somehow jumped mempool sequence numbers! " f"zmq_mem_seq: {zmq_mem_seq} > " f"get_raw_seq: {get_raw_seq}") # 4) Moving forward, we apply the delta to our local view # remaining txs + conflict (A, R, C) + 1 block connect + 1 final tx expected_sequence = get_raw_seq for _ in range(num_txs + 3 + 1 + 1): (hash_str, label, mempool_sequence) = seq.receive_sequence() if label == "A": assert hash_str not in mempool_view mempool_view.add(hash_str) expected_sequence = mempool_sequence + 1 elif label == "R": assert hash_str in mempool_view mempool_view.remove(hash_str) expected_sequence = mempool_sequence + 1 elif label == "C": # (Attempt to) remove all txids from known block connects block_txids = self.nodes[0].getblock(hash_str)["tx"][1:] for txid in block_txids: if txid in mempool_view: expected_sequence += 1 mempool_view.remove(txid) elif label == "D": # Not useful for mempool tracking per se continue else: raise Exception("Unexpected ZMQ sequence label!") assert_equal(self.nodes[0].getrawmempool(), [final_txid]) assert_equal( self.nodes[0].getrawmempool( mempool_sequence=True)["mempool_sequence"], expected_sequence) # 5) If you miss a zmq/mempool sequence number, go back to step (2) self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) def test_multiple_interfaces(self): # Set up two subscribers with different addresses # (note that after the reorg test, syncing would fail due to different # chain lengths on node0 and node1; for this test we only need node0, so # we can disable syncing blocks on the setup) subscribers = self.setup_zmq_test([ ("hashblock", "tcp://127.0.0.1:28334"), ("hashblock", "tcp://127.0.0.1:28335"), ], sync_blocks=False) # Generate 1 block in nodes[0] and receive all notifications self.generatetoaddress( self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op) # Should receive the same block hash on both subscribers assert_equal(self.nodes[0].getbestblockhash(), subscribers[0].receive().hex()) assert_equal(self.nodes[0].getbestblockhash(), subscribers[1].receive().hex()) if __name__ == '__main__': ZMQTest().main() diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index 8813cf760..71511f28a 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -1,379 +1,376 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test descendant package tracking code.""" from decimal import Decimal from test_framework.messages import XEC from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, satoshi_round, ) # default limits MAX_ANCESTORS = 50 MAX_DESCENDANTS = 50 # custom limits for node1 MAX_ANCESTORS_CUSTOM = 5 class MempoolPackagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 common_params = ["-maxorphantx=1000"] self.extra_args = [ common_params, common_params + ["-limitancestorcount={}".format(MAX_ANCESTORS_CUSTOM)]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() # Build a transaction that spends parent_txid:vout # Return amount sent def chain_transaction(self, node, parent_txid, vout, value, fee, num_outputs): send_value = satoshi_round((value - fee) / num_outputs) inputs = [{'txid': parent_txid, 'vout': vout}] outputs = {} for _ in range(num_outputs): outputs[node.getnewaddress()] = send_value rawtx = node.createrawtransaction(inputs, outputs) signedtx = node.signrawtransactionwithwallet(rawtx) txid = node.sendrawtransaction(signedtx['hex']) fulltx = node.getrawtransaction(txid, 1) # make sure we didn't generate a change output assert len(fulltx['vout']) == num_outputs return (txid, send_value) def run_test(self): # Mine some blocks and have them mature. # keep track of invs peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) self.generate(self.nodes[0], 101) utxo = self.nodes[0].listunspent(10) txid = utxo[0]['txid'] vout = utxo[0]['vout'] value = utxo[0]['amount'] assert 'ancestorcount' not in utxo[0] assert 'ancestorsize' not in utxo[0] assert 'ancestorfees' not in utxo[0] fee = Decimal("100") # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] ancestor_size = 0 ancestor_fees = Decimal(0) for i in range(MAX_ANCESTORS): (txid, sent_value) = self.chain_transaction( self.nodes[0], txid, 0, value, fee, 1) value = sent_value chain.append(txid) # Check that listunspent ancestor{count, size, fees} yield the # correct results wallet_unspent = self.nodes[0].listunspent(minconf=0) this_unspent = next( utxo_info for utxo_info in wallet_unspent if utxo_info['txid'] == txid) assert_equal(this_unspent['ancestorcount'], i + 1) ancestor_size += self.nodes[0].getrawtransaction( txid=txid, verbose=True)['size'] assert_equal(this_unspent['ancestorsize'], ancestor_size) ancestor_fees -= self.nodes[0].gettransaction(txid=txid)['fee'] assert_equal(this_unspent['ancestorfees'], ancestor_fees) # Wait until mempool transactions have passed initial broadcast # (sent inv and received getdata) # Otherwise, getrawmempool may be inconsistent with getmempoolentry if # unbroadcast changes in between peer_inv_store.wait_for_broadcast(chain) # Check mempool has MAX_ANCESTORS transactions in it, and descendant and ancestor # count and fees should look correct mempool = self.nodes[0].getrawmempool(True) assert_equal(len(mempool), MAX_ANCESTORS) descendant_count = 1 descendant_fees = 0 descendant_size = 0 assert_equal(ancestor_size, sum([mempool[tx]['size'] for tx in mempool])) ancestor_count = MAX_ANCESTORS assert_equal(ancestor_fees, sum([mempool[tx]['fee'] for tx in mempool])) descendants = [] ancestors = list(chain) for x in reversed(chain): # Check that getmempoolentry is consistent with getrawmempool entry = self.nodes[0].getmempoolentry(x) assert_equal(entry, mempool[x]) # Check that the descendant calculations are correct assert_equal(mempool[x]['descendantcount'], descendant_count) descendant_fees += mempool[x]['fee'] assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee']) assert_equal(mempool[x]['fees']['base'], mempool[x]['fee']) assert_equal(mempool[x]['fees']['modified'], mempool[x]['modifiedfee']) assert_equal(mempool[x]['descendantfees'], descendant_fees * XEC) assert_equal(mempool[x]['fees']['descendant'], descendant_fees) descendant_size += mempool[x]['size'] assert_equal(mempool[x]['descendantsize'], descendant_size) descendant_count += 1 # Check that ancestor calculations are correct assert_equal(mempool[x]['ancestorcount'], ancestor_count) assert_equal(mempool[x]['ancestorfees'], ancestor_fees * XEC) assert_equal(mempool[x]['ancestorsize'], ancestor_size) ancestor_size -= mempool[x]['size'] ancestor_fees -= mempool[x]['fee'] ancestor_count -= 1 # Check that parent/child list is correct assert_equal(mempool[x]['spentby'], descendants[-1:]) assert_equal(mempool[x]['depends'], ancestors[-2:-1]) # Check that getmempooldescendants is correct assert_equal(sorted(descendants), sorted( self.nodes[0].getmempooldescendants(x))) # Check getmempooldescendants verbose output is correct for descendant, dinfo in self.nodes[0].getmempooldescendants( x, True).items(): assert_equal(dinfo['depends'], [ chain[chain.index(descendant) - 1]]) if dinfo['descendantcount'] > 1: assert_equal(dinfo['spentby'], [ chain[chain.index(descendant) + 1]]) else: assert_equal(dinfo['spentby'], []) descendants.append(x) # Check that getmempoolancestors is correct ancestors.remove(x) assert_equal(sorted(ancestors), sorted( self.nodes[0].getmempoolancestors(x))) # Check that getmempoolancestors verbose output is correct for ancestor, ainfo in self.nodes[0].getmempoolancestors( x, True).items(): assert_equal(ainfo['spentby'], [ chain[chain.index(ancestor) + 1]]) if ainfo['ancestorcount'] > 1: assert_equal(ainfo['depends'], [ chain[chain.index(ancestor) - 1]]) else: assert_equal(ainfo['depends'], []) # Check that getmempoolancestors/getmempooldescendants correctly handle # verbose=true v_ancestors = self.nodes[0].getmempoolancestors(chain[-1], True) assert_equal(len(v_ancestors), len(chain) - 1) for x in v_ancestors.keys(): assert_equal(mempool[x], v_ancestors[x]) assert chain[-1] not in v_ancestors.keys() v_descendants = self.nodes[0].getmempooldescendants(chain[0], True) assert_equal(len(v_descendants), len(chain) - 1) for x in v_descendants.keys(): assert_equal(mempool[x], v_descendants[x]) assert chain[0] not in v_descendants.keys() # Check that ancestor modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=1000) mempool = self.nodes[0].getrawmempool(True) ancestor_fees = 0 for x in chain: ancestor_fees += mempool[x]['fee'] assert_equal(mempool[x]['fees']['ancestor'], ancestor_fees + Decimal('10.00')) assert_equal(mempool[x]['ancestorfees'], ancestor_fees * XEC + 1000) # Undo the prioritisetransaction for later tests self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=-1000) # Check that descendant modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=1000) mempool = self.nodes[0].getrawmempool(True) descendant_fees = 0 for x in reversed(chain): descendant_fees += mempool[x]['fee'] assert_equal(mempool[x]['fees']['descendant'], descendant_fees + Decimal('10.00')) assert_equal(mempool[x]['descendantfees'], descendant_fees * XEC + 1000) # Adding one more transaction on to the chain should fail. assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], txid, vout, value, fee, 1) # Check that prioritising a tx before it's added to the mempool works # First clear the mempool by mining a block. self.generate(self.nodes[0], 1) - self.sync_blocks() assert_equal(len(self.nodes[0].getrawmempool()), 0) # Prioritise a transaction that has been mined, then add it back to the # mempool by using invalidateblock. self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=2000) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) # Keep node1's tip synced with node0 self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) # Now check that the transaction is in the mempool, with the right # modified fee mempool = self.nodes[0].getrawmempool(True) descendant_fees = 0 for x in reversed(chain): descendant_fees += mempool[x]['fee'] if (x == chain[-1]): assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee'] + satoshi_round(20.00)) assert_equal(mempool[x]['fees']['modified'], mempool[x]['fee'] + satoshi_round(20.00)) assert_equal(mempool[x]['descendantfees'], descendant_fees * XEC + 2000) assert_equal(mempool[x]['fees']['descendant'], descendant_fees + satoshi_round(20.00)) # Check that node1's mempool is as expected (-> custom ancestor limit) mempool0 = self.nodes[0].getrawmempool(False) mempool1 = self.nodes[1].getrawmempool(False) assert_equal(len(mempool1), MAX_ANCESTORS_CUSTOM) assert set(mempool1).issubset(set(mempool0)) for tx in chain[:MAX_ANCESTORS_CUSTOM]: assert tx in mempool1 # TODO: more detailed check of node1's mempool (fees etc.) # check transaction unbroadcast info (should be false if in both # mempools) mempool = self.nodes[0].getrawmempool(True) for tx in mempool: assert_equal(mempool[tx]['unbroadcast'], False) # TODO: test ancestor size limits # Now test descendant chain limits txid = utxo[1]['txid'] value = utxo[1]['amount'] vout = utxo[1]['vout'] transaction_package = [] tx_children = [] # First create one parent tx with 10 children (txid, sent_value) = self.chain_transaction( self.nodes[0], txid, vout, value, fee, 10) parent_transaction = txid for i in range(10): transaction_package.append( {'txid': txid, 'vout': i, 'amount': sent_value}) # Sign and send up to MAX_DESCENDANT transactions chained off the # parent tx for _ in range(MAX_DESCENDANTS - 1): utxo = transaction_package.pop(0) (txid, sent_value) = self.chain_transaction( self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) if utxo['txid'] is parent_transaction: tx_children.append(txid) for j in range(10): transaction_package.append( {'txid': txid, 'vout': j, 'amount': sent_value}) mempool = self.nodes[0].getrawmempool(True) assert_equal(mempool[parent_transaction] ['descendantcount'], MAX_DESCENDANTS) assert_equal(sorted(mempool[parent_transaction] ['spentby']), sorted(tx_children)) for child in tx_children: assert_equal(mempool[child]['depends'], [parent_transaction]) # Sending one more chained transaction will fail utxo = transaction_package.pop(0) assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) # TODO: check that node1's mempool is as expected # TODO: test descendant size limits # Test reorg handling # First, the basics: self.generate(self.nodes[0], 1) - self.sync_blocks() self.nodes[1].invalidateblock(self.nodes[0].getbestblockhash()) self.nodes[1].reconsiderblock(self.nodes[0].getbestblockhash()) # Now test the case where node1 has a transaction T in its mempool that # depends on transactions A and B which are in a mined block, and the # block containing A and B is disconnected, AND B is not accepted back # into node1's mempool because its ancestor count is too high. # Create 8 transactions, like so: # Tx0 -> Tx1 (vout0) # \--> Tx2 (vout1) -> Tx3 -> Tx4 -> Tx5 -> Tx6 -> Tx7 # # Mine them in the next block, then generate a new tx8 that spends # Tx1 and Tx7, and add to node1's mempool, then disconnect the # last block. # Create tx0 with 2 outputs utxo = self.nodes[0].listunspent() txid = utxo[0]['txid'] value = utxo[0]['amount'] vout = utxo[0]['vout'] send_value = satoshi_round((value - fee) / 2) inputs = [{'txid': txid, 'vout': vout}] outputs = {} for _ in range(2): outputs[self.nodes[0].getnewaddress()] = send_value rawtx = self.nodes[0].createrawtransaction(inputs, outputs) signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) txid = self.nodes[0].sendrawtransaction(signedtx['hex']) tx0_id = txid value = send_value # Create tx1 tx1_id, _ = self.chain_transaction( self.nodes[0], tx0_id, 0, value, fee, 1) # Create tx2-7 vout = 1 txid = tx0_id for _ in range(6): (txid, sent_value) = self.chain_transaction( self.nodes[0], txid, vout, value, fee, 1) vout = 0 value = sent_value # Mine these in a block self.generate(self.nodes[0], 1) - self.sync_all() # Now generate tx8, with a big fee inputs = [{'txid': tx1_id, 'vout': 0}, {'txid': txid, 'vout': 0}] outputs = {self.nodes[0].getnewaddress(): send_value + value - 4 * fee} rawtx = self.nodes[0].createrawtransaction(inputs, outputs) signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) txid = self.nodes[0].sendrawtransaction(signedtx['hex']) self.sync_mempools() # Now try to disconnect the tip on each node... self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) self.sync_blocks() if __name__ == '__main__': MempoolPackagesTest().main() diff --git a/test/functional/mining_getblocktemplate_longpoll.py b/test/functional/mining_getblocktemplate_longpoll.py index 91bf69967..9af638d68 100755 --- a/test/functional/mining_getblocktemplate_longpoll.py +++ b/test/functional/mining_getblocktemplate_longpoll.py @@ -1,99 +1,98 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test longpolling with getblocktemplate.""" import random import threading from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import get_rpc_proxy from test_framework.wallet import MiniWallet class LongpollThread(threading.Thread): def __init__(self, node): threading.Thread.__init__(self) # query current longpollid templat = node.getblocktemplate() self.longpollid = templat['longpollid'] # create a new connection to the node, we can't use the same # connection from two threads self.node = get_rpc_proxy( node.url, 1, timeout=600, coveragedir=node.coverage_dir) def run(self): self.node.getblocktemplate({'longpollid': self.longpollid}) class GetBlockTemplateLPTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.supports_cli = False 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.generate(self.nodes[0], 10) templat = self.nodes[0].getblocktemplate() longpollid = templat['longpollid'] # longpollid should not change between successive invocations if # nothing else happens templat2 = self.nodes[0].getblocktemplate() assert templat2['longpollid'] == longpollid self.log.info("Test that longpoll waits if we do nothing") thr = LongpollThread(self.nodes[0]) thr.start() # check that thread still lives # wait 5 seconds or until thread exits thr.join(5) assert thr.is_alive() miniwallets = [MiniWallet(node) for node in self.nodes] self.log.info( "Test that longpoll will terminate if another node generates a block") # generate a block on another node self.generate(miniwallets[1], 1) # check that thread will exit now that new transaction entered mempool # wait 5 seconds or until thread exits thr.join(5) assert not thr.is_alive() self.log.info( "Test that longpoll will terminate if we generate a block ourselves") thr = LongpollThread(self.nodes[0]) thr.start() # generate a block on own node self.generate(miniwallets[0], 1) # wait 5 seconds or until thread exits thr.join(5) assert not thr.is_alive() # Add enough mature utxos to the wallets, so that all txs spend # confirmed coins self.generate(self.nodes[0], 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"] fee_rate = min_relay_fee + Decimal('0.10') * 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() if __name__ == '__main__': GetBlockTemplateLPTest().main() diff --git a/test/functional/p2p_blockfilters.py b/test/functional/p2p_blockfilters.py index 9cddd0cb3..7d8a26433 100755 --- a/test/functional/p2p_blockfilters.py +++ b/test/functional/p2p_blockfilters.py @@ -1,267 +1,266 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests NODE_COMPACT_FILTERS (BIP 157/158). Tests that a node configured with -blockfilterindex and -peerblockfilters signals NODE_COMPACT_FILTERS and can serve cfilters, cfheaders and cfcheckpts. """ from test_framework.messages import ( FILTER_TYPE_BASIC, NODE_COMPACT_FILTERS, hash256, msg_getcfcheckpt, msg_getcfheaders, msg_getcfilters, ser_uint256, uint256_from_str, ) from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class FiltersClient(P2PInterface): def __init__(self): super().__init__() # Store the cfilters received. self.cfilters = [] def pop_cfilters(self): cfilters = self.cfilters self.cfilters = [] return cfilters def on_cfilter(self, message): """Store cfilters received in a list.""" self.cfilters.append(message) class CompactFiltersTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.rpc_timeout = 480 self.num_nodes = 2 self.extra_args = [ ["-blockfilterindex", "-peerblockfilters"], ["-blockfilterindex"], ] def run_test(self): # Node 0 supports COMPACT_FILTERS, node 1 does not. peer_0 = self.nodes[0].add_p2p_connection(FiltersClient()) peer_1 = self.nodes[1].add_p2p_connection(FiltersClient()) # Nodes 0 & 1 share the same first 999 blocks in the chain. self.generate(self.nodes[0], 999) - self.sync_blocks(timeout=600) # Stale blocks by disconnecting nodes 0 & 1, mining, then reconnecting self.disconnect_nodes(0, 1) stale_block_hash = self.generate( self.nodes[0], 1, sync_fun=self.no_op)[0] self.nodes[0].syncwithvalidationinterfacequeue() assert_equal(self.nodes[0].getblockcount(), 1000) self.generate(self.nodes[1], 1001, sync_fun=self.no_op) assert_equal(self.nodes[1].getblockcount(), 2000) # Check that nodes have signalled NODE_COMPACT_FILTERS correctly. assert peer_0.nServices & NODE_COMPACT_FILTERS != 0 assert peer_1.nServices & NODE_COMPACT_FILTERS == 0 # Check that the localservices is as expected. assert int( self.nodes[0].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS != 0 assert int( self.nodes[1].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS == 0 self.log.info("get cfcheckpt on chain to be re-orged out.") request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, stop_hash=int(stale_block_hash, 16), ) peer_0.send_and_ping(message=request) response = peer_0.last_message['cfcheckpt'] assert_equal(response.filter_type, request.filter_type) assert_equal(response.stop_hash, request.stop_hash) assert_equal(len(response.headers), 1) self.log.info("Reorg node 0 to a new chain.") self.connect_nodes(0, 1) self.sync_blocks(timeout=600) self.nodes[0].syncwithvalidationinterfacequeue() main_block_hash = self.nodes[0].getblockhash(1000) assert main_block_hash != stale_block_hash, "node 0 chain did not reorganize" self.log.info("Check that peers can fetch cfcheckpt on active chain.") tip_hash = self.nodes[0].getbestblockhash() request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, stop_hash=int(tip_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.last_message['cfcheckpt'] assert_equal(response.filter_type, request.filter_type) assert_equal(response.stop_hash, request.stop_hash) main_cfcheckpt = self.nodes[0].getblockfilter( main_block_hash, 'basic')['header'] tip_cfcheckpt = self.nodes[0].getblockfilter(tip_hash, 'basic')[ 'header'] assert_equal( response.headers, [int(header, 16) for header in (main_cfcheckpt, tip_cfcheckpt)], ) self.log.info("Check that peers can fetch cfcheckpt on stale chain.") request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, stop_hash=int(stale_block_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.last_message['cfcheckpt'] stale_cfcheckpt = self.nodes[0].getblockfilter( stale_block_hash, 'basic')['header'] assert_equal( response.headers, [int(header, 16) for header in (stale_cfcheckpt, )], ) self.log.info("Check that peers can fetch cfheaders on active chain.") request = msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1, stop_hash=int(main_block_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.last_message['cfheaders'] main_cfhashes = response.hashes assert_equal(len(main_cfhashes), 1000) assert_equal( compute_last_header(response.prev_header, response.hashes), int(main_cfcheckpt, 16), ) self.log.info("Check that peers can fetch cfheaders on stale chain.") request = msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1, stop_hash=int(stale_block_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.last_message['cfheaders'] stale_cfhashes = response.hashes assert_equal(len(stale_cfhashes), 1000) assert_equal( compute_last_header(response.prev_header, response.hashes), int(stale_cfcheckpt, 16), ) self.log.info("Check that peers can fetch cfilters.") stop_hash = self.nodes[0].getblockhash(10) request = msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1, stop_hash=int(stop_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.pop_cfilters() assert_equal(len(response), 10) self.log.info("Check that cfilter responses are correct.") for cfilter, cfhash, height in zip( response, main_cfhashes, range(1, 11)): block_hash = self.nodes[0].getblockhash(height) assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC) assert_equal(cfilter.block_hash, int(block_hash, 16)) computed_cfhash = uint256_from_str(hash256(cfilter.filter_data)) assert_equal(computed_cfhash, cfhash) self.log.info("Check that peers can fetch cfilters for stale blocks.") request = msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1000, stop_hash=int(stale_block_hash, 16), ) peer_0.send_and_ping(request) response = peer_0.pop_cfilters() assert_equal(len(response), 1) cfilter = response[0] assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC) assert_equal(cfilter.block_hash, int(stale_block_hash, 16)) computed_cfhash = uint256_from_str(hash256(cfilter.filter_data)) assert_equal(computed_cfhash, stale_cfhashes[999]) self.log.info( "Requests to node 1 without NODE_COMPACT_FILTERS results in disconnection.") requests = [ msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, stop_hash=int(main_block_hash, 16), ), msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1000, stop_hash=int(main_block_hash, 16), ), msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1000, stop_hash=int(main_block_hash, 16), ), ] for request in requests: peer_1 = self.nodes[1].add_p2p_connection(P2PInterface()) peer_1.send_message(request) peer_1.wait_for_disconnect() self.log.info("Check that invalid requests result in disconnection.") requests = [ # Requesting too many filters results in disconnection. msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=0, stop_hash=int(main_block_hash, 16), ), # Requesting too many filter headers results in disconnection. msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=0, stop_hash=int(tip_hash, 16), ), # Requesting unknown filter type results in disconnection. msg_getcfcheckpt( filter_type=255, stop_hash=int(main_block_hash, 16), ), # Requesting unknown hash results in disconnection. msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, stop_hash=123456789, ), ] for request in requests: peer_0 = self.nodes[0].add_p2p_connection(P2PInterface()) peer_0.send_message(request) peer_0.wait_for_disconnect() def compute_last_header(prev_header, hashes): """Compute the last filter header from a starting header and a sequence of filter hashes.""" header = ser_uint256(prev_header) for filter_hash in hashes: header = hash256(ser_uint256(filter_hash) + header) return uint256_from_str(header) if __name__ == '__main__': CompactFiltersTest().main() diff --git a/test/functional/p2p_ibd_txrelay.py b/test/functional/p2p_ibd_txrelay.py index 08954373e..d60ccca60 100755 --- a/test/functional/p2p_ibd_txrelay.py +++ b/test/functional/p2p_ibd_txrelay.py @@ -1,47 +1,46 @@ #!/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 fee filters during and after IBD.""" from decimal import Decimal from test_framework.messages import XEC from test_framework.test_framework import BitcoinTestFramework MAX_FEE_FILTER = Decimal(9170997) / XEC NORMAL_FEE_FILTER = Decimal(100) / XEC class P2PIBDTxRelayTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ ["-minrelaytxfee={}".format(NORMAL_FEE_FILTER)], ["-minrelaytxfee={}".format(NORMAL_FEE_FILTER)], ] def run_test(self): self.log.info( "Check that nodes set minfilter to MAX_MONEY while still in IBD") for node in self.nodes: assert node.getblockchaininfo()['initialblockdownload'] self.wait_until(lambda: all(peer['minfeefilter'] == MAX_FEE_FILTER for peer in node.getpeerinfo())) # Come out of IBD by generating a block self.generate(self.nodes[0], 1) - self.sync_all() self.log.info( "Check that nodes reset minfilter after coming out of IBD") for node in self.nodes: assert not node.getblockchaininfo()['initialblockdownload'] self.wait_until( lambda: all(peer['minfeefilter'] == NORMAL_FEE_FILTER for peer in node.getpeerinfo())) if __name__ == '__main__': P2PIBDTxRelayTest().main() diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 3f0fc581c..217955b97 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -1,214 +1,213 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2018 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 p2p permission message. Test that permissions are correctly calculated and applied """ from test_framework.address import ( ADDRESS_ECREG_P2SH_OP_TRUE, SCRIPTSIG_OP_TRUE, ) from test_framework.messages import CTransaction, FromHex from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.txtools import pad_tx from test_framework.util import assert_equal, p2p_port class P2PPermissionsTests(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True def run_test(self): self.check_tx_relay() self.checkpermission( # default permissions (no specific permissions) ["-whitelist=127.0.0.1"], # Make sure the default values in the command line documentation # match the ones here ["relay", "noban", "mempool", "download"], ) 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"], ) self.checkpermission( # no permission (even with forcerelay) ["-whitelist=@127.0.0.1", "-whitelistforcerelay=1"], [], ) self.checkpermission( # relay permission removed (no specific permissions) ["-whitelist=127.0.0.1", "-whitelistrelay=0"], ["noban", "mempool", "download"], ) self.checkpermission( # forcerelay and relay permission added # Legacy parameter interaction which set whitelistrelay to true # if whitelistforcerelay is true ["-whitelist=127.0.0.1", "-whitelistforcerelay"], ["forcerelay", "relay", "noban", "mempool", "download"], ) # Let's make sure permissions are merged correctly # For this, we need to use whitebind instead of bind # by modifying the configuration file. ip_port = "127.0.0.1:{}".format(p2p_port(1)) self.replaceinconfig( 1, "bind=127.0.0.1", "whitebind=bloomfilter,forcerelay@" + ip_port) self.checkpermission( ["-whitelist=noban@127.0.0.1"], # Check parameter interaction forcerelay should activate relay ["noban", "bloomfilter", "forcerelay", "relay", "download"], ) self.replaceinconfig( 1, "whitebind=bloomfilter,forcerelay@" + ip_port, "bind=127.0.0.1") self.checkpermission( # legacy whitelistrelay should be ignored ["-whitelist=noban,mempool@127.0.0.1", "-whitelistrelay"], ["noban", "mempool", "download"], ) self.checkpermission( # check without deprecatedrpc=whitelisted ["-whitelist=noban,mempool@127.0.0.1", "-whitelistrelay"], ["noban", "mempool", "download"], ) self.checkpermission( # legacy whitelistforcerelay should be ignored ["-whitelist=noban,mempool@127.0.0.1", "-whitelistforcerelay"], ["noban", "mempool", "download"], ) self.checkpermission( # missing mempool permission to be considered legacy whitelisted ["-whitelist=noban@127.0.0.1"], ["noban", "download"], ) self.checkpermission( # all permission added ["-whitelist=all@127.0.0.1"], ["forcerelay", "noban", "mempool", "bloomfilter", "relay", "download", "bypass_proof_request_limits", "addr"], ) self.checkpermission( # bypass_proof_request_limits permission ["-whitelist=bypass_proof_request_limits@127.0.0.1"], ["bypass_proof_request_limits"], ) self.stop_node(1) self.nodes[1].assert_start_raises_init_error( ["-whitelist=oopsie@127.0.0.1"], "Invalid P2P permission", match=ErrorMatch.PARTIAL_REGEX) self.nodes[1].assert_start_raises_init_error( ["-whitelist=noban@127.0.0.1:230"], "Invalid netmask specified in", match=ErrorMatch.PARTIAL_REGEX) self.nodes[1].assert_start_raises_init_error( ["-whitebind=noban@127.0.0.1/10"], "Cannot resolve -whitebind address", match=ErrorMatch.PARTIAL_REGEX) def check_tx_relay(self): block_op_true = self.nodes[0].getblock( self.generatetoaddress(self.nodes[0], 100, ADDRESS_ECREG_P2SH_OP_TRUE)[0]) - self.sync_all() self.log.debug( "Create a connection from a forcerelay peer that rebroadcasts raw txs") # A python mininode is needed to send the raw transaction directly. # If a full node was used, it could only rebroadcast via the inv-getdata # mechanism. However, even for forcerelay connections, a full node would # currently not request a txid that is already in the mempool. self.restart_node(1, extra_args=["-whitelist=forcerelay@127.0.0.1"]) p2p_rebroadcast_wallet = self.nodes[1].add_p2p_connection( P2PDataStore()) self.log.debug("Send a tx from the wallet initially") tx = FromHex(CTransaction(), self.nodes[0].createrawtransaction( inputs=[{'txid': block_op_true['tx'][0], 'vout': 0}], outputs=[{ADDRESS_ECREG_P2SH_OP_TRUE: 50}])) # push the one byte script to the stack tx.vin[0].scriptSig = SCRIPTSIG_OP_TRUE pad_tx(tx) txid = tx.rehash() self.log.debug("Wait until tx is in node[1]'s mempool") 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") 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()) 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=f'{txid} from peer=0 was not accepted: ' f'txn-mempool-conflict', ) 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), ) def checkpermission(self, args, expectedPermissions): self.restart_node(1, args) self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] assert_equal(len(expectedPermissions), len(peerinfo['permissions'])) for p in expectedPermissions: if p not in peerinfo['permissions']: raise AssertionError( "Expected permissions {!r} is not granted.".format(p)) def replaceinconfig(self, nodeid, old, new): with open(self.nodes[nodeid].bitcoinconf, encoding="utf8") as f: newText = f.read().replace(old, new) with open(self.nodes[nodeid].bitcoinconf, 'w', encoding="utf8") as f: f.write(newText) if __name__ == '__main__': P2PPermissionsTests().main() diff --git a/test/functional/p2p_sendheaders.py b/test/functional/p2p_sendheaders.py index 909e738a0..ba8c2cd90 100755 --- a/test/functional/p2p_sendheaders.py +++ b/test/functional/p2p_sendheaders.py @@ -1,630 +1,628 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2017 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 behavior of headers messages to announce blocks. Setup: - Two nodes: - node0 is the node-under-test. We create two p2p connections to it. The first p2p connection is a control and should only ever receive inv's. The second p2p connection tests the headers sending logic. - node1 is used to create reorgs. test_null_locators ================== Sends two getheaders requests with null locator values. First request's hashstop value refers to validated block, while second request's hashstop value refers to a block which hasn't been validated. Verifies only the first request returns headers. test_nonnull_locators ===================== Part 1: No headers announcements before "sendheaders" a. node mines a block [expect: inv] send getdata for the block [expect: block] b. node mines another block [expect: inv] send getheaders and getdata [expect: headers, then block] c. node mines another block [expect: inv] peer mines a block, announces with header [expect: getdata] d. node mines another block [expect: inv] Part 2: After "sendheaders", headers announcements should generally work. a. peer sends sendheaders [expect: no response] peer sends getheaders with current tip [expect: no response] b. node mines a block [expect: tip header] c. for N in 1, ..., 10: * for announce-type in {inv, header} - peer mines N blocks, announces with announce-type [ expect: getheaders/getdata or getdata, deliver block(s) ] - node mines a block [ expect: 1 header ] Part 3: Headers announcements stop after large reorg and resume after getheaders or inv from peer. - For response-type in {inv, getheaders} * node mines a 7 block reorg [ expect: headers announcement of 8 blocks ] * node mines an 8-block reorg [ expect: inv at tip ] * peer responds with getblocks/getdata [expect: inv, blocks ] * node mines another block [ expect: inv at tip, peer sends getdata, expect: block ] * node mines another block at tip [ expect: inv ] * peer responds with getheaders with an old hashstop more than 8 blocks back [expect: headers] * peer requests block [ expect: block ] * node mines another block at tip [ expect: inv, peer sends getdata, expect: block ] * peer sends response-type [expect headers if getheaders, getheaders/getdata if mining new block] * node mines 1 block [expect: 1 header, peer responds with getdata] Part 4: Test direct fetch behavior a. Announce 2 old block headers. Expect: no getdata requests. b. Announce 3 new blocks via 1 headers message. Expect: one getdata request for all 3 blocks. (Send blocks.) c. Announce 1 header that forks off the last two blocks. Expect: no response. d. Announce 1 more header that builds on that fork. Expect: one getdata request for two blocks. e. Announce 16 more headers that build on that fork. Expect: getdata request for 14 more blocks. f. Announce 1 more header that builds on that fork. Expect: no response. Part 5: Test handling of headers that don't connect. a. Repeat 10 times: 1. Announce a header that doesn't connect. Expect: getheaders message 2. Send headers chain. Expect: getdata for the missing blocks, tip update. b. Then send 9 more headers that don't connect. Expect: getheaders message each time. c. Announce a header that does connect. Expect: no response. d. Announce 49 headers that don't connect. Expect: getheaders message each time. e. Announce one more that doesn't connect. Expect: disconnect. """ from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import ( MSG_BLOCK, CBlockHeader, CInv, msg_block, msg_getblocks, msg_getdata, msg_getheaders, msg_headers, msg_inv, msg_sendheaders, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal DIRECT_FETCH_RESPONSE_TIME = 0.05 class BaseNode(P2PInterface): def __init__(self): super().__init__() self.block_announced = False self.last_blockhash_announced = None self.recent_headers_announced = [] def send_get_data(self, block_hashes): """Request data for a list of block hashes.""" msg = msg_getdata() for x in block_hashes: msg.inv.append(CInv(MSG_BLOCK, x)) self.send_message(msg) def send_get_headers(self, locator, hashstop): msg = msg_getheaders() msg.locator.vHave = locator msg.hashstop = hashstop self.send_message(msg) def send_block_inv(self, blockhash): msg = msg_inv() msg.inv = [CInv(MSG_BLOCK, blockhash)] self.send_message(msg) def send_header_for_blocks(self, new_blocks): headers_message = msg_headers() headers_message.headers = [CBlockHeader(b) for b in new_blocks] self.send_message(headers_message) def send_getblocks(self, locator): getblocks_message = msg_getblocks() getblocks_message.locator.vHave = locator self.send_message(getblocks_message) def wait_for_block_announcement(self, block_hash, timeout=60): def test_function(): return self.last_blockhash_announced == block_hash self.wait_until(test_function, timeout=timeout) def on_inv(self, message): self.block_announced = True self.last_blockhash_announced = message.inv[-1].hash def on_headers(self, message): if len(message.headers): self.block_announced = True for x in message.headers: x.calc_sha256() # append because headers may be announced over multiple # messages. self.recent_headers_announced.append(x.sha256) self.last_blockhash_announced = message.headers[-1].sha256 def clear_block_announcements(self): with p2p_lock: self.block_announced = False self.last_message.pop("inv", None) self.last_message.pop("headers", None) self.recent_headers_announced = [] def check_last_headers_announcement(self, headers): """Test whether the last headers announcements received are right. Headers may be announced across more than one message.""" def test_function(): return (len(self.recent_headers_announced) >= len(headers)) self.wait_until(test_function) with p2p_lock: assert_equal(self.recent_headers_announced, headers) self.block_announced = False self.last_message.pop("headers", None) self.recent_headers_announced = [] def check_last_inv_announcement(self, inv): """Test whether the last announcement received had the right inv. inv should be a list of block hashes.""" def test_function(): return self.block_announced self.wait_until(test_function) with p2p_lock: compare_inv = [] if "inv" in self.last_message: compare_inv = [x.hash for x in self.last_message["inv"].inv] assert_equal(compare_inv, inv) self.block_announced = False self.last_message.pop("inv", None) class SendHeadersTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"]] def mine_blocks(self, count): """Mine count blocks and return the new tip.""" # Clear out block announcements from each p2p listener [x.clear_block_announcements() for x in self.nodes[0].p2ps] self.generatetoaddress(self.nodes[0], count, self.nodes[0].get_deterministic_priv_key().address) return int(self.nodes[0].getbestblockhash(), 16) def mine_reorg(self, length): """Mine a reorg that invalidates length blocks (replacing them with # length+1 blocks). Note: we clear the state of our p2p connections after the to-be-reorged-out blocks are mined, so that we don't break later tests. return the list of block hashes newly mined.""" # make sure all invalidated blocks are node0's self.generatetoaddress(self.nodes[0], length, self.nodes[0].get_deterministic_priv_key().address) - self.sync_blocks(self.nodes, wait=0.1) for x in self.nodes[0].p2ps: x.wait_for_block_announcement( int(self.nodes[0].getbestblockhash(), 16)) x.clear_block_announcements() tip_height = self.nodes[1].getblockcount() hash_to_invalidate = self.nodes[1].getblockhash( tip_height - (length - 1)) self.nodes[1].invalidateblock(hash_to_invalidate) # Must be longer than the orig chain all_hashes = self.generatetoaddress(self.nodes[1], length + 1, self.nodes[1].get_deterministic_priv_key().address) - self.sync_blocks(self.nodes, wait=0.1) return [int(x, 16) for x in all_hashes] def run_test(self): # Setup the p2p connections inv_node = self.nodes[0].add_p2p_connection(BaseNode()) # Make sure NODE_NETWORK is not set for test_node, so no block download # will occur outside of direct fetching test_node = self.nodes[0].add_p2p_connection(BaseNode(), services=0) self.test_null_locators(test_node, inv_node) self.test_nonnull_locators(test_node, inv_node) def test_null_locators(self, test_node, inv_node): tip = self.nodes[0].getblockheader(self.generatetoaddress(self.nodes[0], 1, self.nodes[0].get_deterministic_priv_key().address)[0]) tip_hash = int(tip["hash"], 16) inv_node.check_last_inv_announcement(inv=[tip_hash]) test_node.check_last_inv_announcement(inv=[tip_hash]) self.log.info( "Verify getheaders with null locator and valid hashstop returns headers.") test_node.clear_block_announcements() test_node.send_get_headers(locator=[], hashstop=tip_hash) test_node.check_last_headers_announcement(headers=[tip_hash]) self.log.info( "Verify getheaders with null locator and invalid hashstop does not return headers.") block = create_block(int(tip["hash"], 16), create_coinbase( tip["height"] + 1), tip["mediantime"] + 1) block.solve() test_node.send_header_for_blocks([block]) test_node.clear_block_announcements() test_node.send_get_headers(locator=[], hashstop=int(block.hash, 16)) test_node.sync_with_ping() assert_equal(test_node.block_announced, False) inv_node.clear_block_announcements() test_node.send_message(msg_block(block)) inv_node.check_last_inv_announcement(inv=[int(block.hash, 16)]) def test_nonnull_locators(self, test_node, inv_node): tip = int(self.nodes[0].getbestblockhash(), 16) # PART 1 # 1. Mine a block; expect inv announcements each time self.log.info( "Part 1: headers don't start before sendheaders message...") for i in range(4): self.log.debug("Part 1.{}: starting...".format(i)) old_tip = tip tip = self.mine_blocks(1) inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_inv_announcement(inv=[tip]) # Try a few different responses; none should affect next # announcement if i == 0: # first request the block test_node.send_get_data([tip]) test_node.wait_for_block(tip) elif i == 1: # next try requesting header and block test_node.send_get_headers(locator=[old_tip], hashstop=tip) test_node.send_get_data([tip]) test_node.wait_for_block(tip) # since we requested headers... test_node.clear_block_announcements() elif i == 2: # this time announce own block via headers inv_node.clear_block_announcements() height = self.nodes[0].getblockcount() last_time = self.nodes[0].getblock( self.nodes[0].getbestblockhash())['time'] block_time = last_time + 1 new_block = create_block( tip, create_coinbase(height + 1), block_time) new_block.solve() test_node.send_header_for_blocks([new_block]) test_node.wait_for_getdata([new_block.sha256]) # make sure this block is processed test_node.send_and_ping(msg_block(new_block)) inv_node.wait_until(lambda: inv_node.block_announced) inv_node.clear_block_announcements() test_node.clear_block_announcements() self.log.info("Part 1: success!") self.log.info( "Part 2: announce blocks with headers after sendheaders message...") # PART 2 # 2. Send a sendheaders message and test that headers announcements # commence and keep working. test_node.send_message(msg_sendheaders()) prev_tip = int(self.nodes[0].getbestblockhash(), 16) test_node.send_get_headers(locator=[prev_tip], hashstop=0) test_node.sync_with_ping() # Now that we've synced headers, headers announcements should work tip = self.mine_blocks(1) inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_headers_announcement(headers=[tip]) height = self.nodes[0].getblockcount() + 1 block_time += 10 # Advance far enough ahead for i in range(10): self.log.debug("Part 2.{}: starting...".format(i)) # Mine i blocks, and alternate announcing either via # inv (of tip) or via headers. After each, new blocks # mined by the node should successfully be announced # with block header, even though the blocks are never requested for j in range(2): self.log.debug("Part 2.{}.{}: starting...".format(i, j)) blocks = [] for _ in range(i + 1): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 if j == 0: # Announce via inv test_node.send_block_inv(tip) test_node.wait_for_getheaders() # Should have received a getheaders now test_node.send_header_for_blocks(blocks) # Test that duplicate inv's won't result in duplicate # getdata requests, or duplicate headers announcements [inv_node.send_block_inv(x.sha256) for x in blocks] test_node.wait_for_getdata([x.sha256 for x in blocks]) inv_node.sync_with_ping() else: # Announce via headers test_node.send_header_for_blocks(blocks) test_node.wait_for_getdata([x.sha256 for x in blocks]) # Test that duplicate headers won't result in duplicate # getdata requests (the check is further down) inv_node.send_header_for_blocks(blocks) inv_node.sync_with_ping() [test_node.send_message(msg_block(x)) for x in blocks] test_node.sync_with_ping() inv_node.sync_with_ping() # This block should not be announced to the inv node (since it also # broadcast it) assert "inv" not in inv_node.last_message assert "headers" not in inv_node.last_message tip = self.mine_blocks(1) inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_headers_announcement(headers=[tip]) height += 1 block_time += 1 self.log.info("Part 2: success!") self.log.info( "Part 3: headers announcements can stop after large reorg, and resume after headers/inv from peer...") # PART 3. Headers announcements can stop after large reorg, and resume after # getheaders or inv from peer. for j in range(2): self.log.debug("Part 3.{}: starting...".format(j)) # First try mining a reorg that can propagate with header # announcement new_block_hashes = self.mine_reorg(length=7) tip = new_block_hashes[-1] inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_headers_announcement(headers=new_block_hashes) block_time += 8 # Mine a too-large reorg, which should be announced with a single # inv new_block_hashes = self.mine_reorg(length=8) tip = new_block_hashes[-1] inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_inv_announcement(inv=[tip]) block_time += 9 fork_point = self.nodes[0].getblock("{:064x}".format( new_block_hashes[0]))["previousblockhash"] fork_point = int(fork_point, 16) # Use getblocks/getdata test_node.send_getblocks(locator=[fork_point]) test_node.check_last_inv_announcement(inv=new_block_hashes) test_node.send_get_data(new_block_hashes) test_node.wait_for_block(new_block_hashes[-1]) for i in range(3): self.log.debug("Part 3.{}.{}: starting...".format(j, i)) # Mine another block, still should get only an inv tip = self.mine_blocks(1) inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_inv_announcement(inv=[tip]) if i == 0: # Just get the data -- shouldn't cause headers # announcements to resume test_node.send_get_data([tip]) test_node.wait_for_block(tip) elif i == 1: # Send a getheaders message that shouldn't trigger headers announcements # to resume (best header sent will be too old) test_node.send_get_headers( locator=[fork_point], hashstop=new_block_hashes[1]) test_node.send_get_data([tip]) test_node.wait_for_block(tip) elif i == 2: # This time, try sending either a getheaders to trigger resumption # of headers announcements, or mine a new block and inv it, also # triggering resumption of headers announcements. test_node.send_get_data([tip]) test_node.wait_for_block(tip) if j == 0: test_node.send_get_headers(locator=[tip], hashstop=0) test_node.sync_with_ping() else: test_node.send_block_inv(tip) test_node.sync_with_ping() # New blocks should now be announced with header tip = self.mine_blocks(1) inv_node.check_last_inv_announcement(inv=[tip]) test_node.check_last_headers_announcement(headers=[tip]) self.log.info("Part 3: success!") self.log.info("Part 4: Testing direct fetch behavior...") tip = self.mine_blocks(1) height = self.nodes[0].getblockcount() + 1 last_time = self.nodes[0].getblock( self.nodes[0].getbestblockhash())['time'] block_time = last_time + 1 # Create 2 blocks. Send the blocks, then send the headers. blocks = [] for _ in range(2): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 inv_node.send_message(msg_block(blocks[-1])) inv_node.sync_with_ping() # Make sure blocks are processed test_node.last_message.pop("getdata", None) test_node.send_header_for_blocks(blocks) test_node.sync_with_ping() # should not have received any getdata messages with p2p_lock: assert "getdata" not in test_node.last_message # This time, direct fetch should work blocks = [] for _ in range(3): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 test_node.send_header_for_blocks(blocks) test_node.sync_with_ping() test_node.wait_for_getdata( [x.sha256 for x in blocks], timeout=DIRECT_FETCH_RESPONSE_TIME) [test_node.send_message(msg_block(x)) for x in blocks] test_node.sync_with_ping() # Now announce a header that forks the last two blocks tip = blocks[0].sha256 height -= 2 blocks = [] # Create extra blocks for later for _ in range(20): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 # Announcing one block on fork should not trigger direct fetch # (less work than tip) test_node.last_message.pop("getdata", None) test_node.send_header_for_blocks(blocks[0:1]) test_node.sync_with_ping() with p2p_lock: assert "getdata" not in test_node.last_message # Announcing one more block on fork should trigger direct fetch for # both blocks (same work as tip) test_node.send_header_for_blocks(blocks[1:2]) test_node.sync_with_ping() test_node.wait_for_getdata( [x.sha256 for x in blocks[0:2]], timeout=DIRECT_FETCH_RESPONSE_TIME) # Announcing 16 more headers should trigger direct fetch for 14 more # blocks test_node.send_header_for_blocks(blocks[2:18]) test_node.sync_with_ping() test_node.wait_for_getdata( [x.sha256 for x in blocks[2:16]], timeout=DIRECT_FETCH_RESPONSE_TIME) # Announcing 1 more header should not trigger any response test_node.last_message.pop("getdata", None) test_node.send_header_for_blocks(blocks[18:19]) test_node.sync_with_ping() with p2p_lock: assert "getdata" not in test_node.last_message self.log.info("Part 4: success!") # Now deliver all those blocks we announced. [test_node.send_message(msg_block(x)) for x in blocks] self.log.info("Part 5: Testing handling of unconnecting headers") # First we test that receipt of an unconnecting header doesn't prevent # chain sync. for i in range(10): self.log.debug("Part 5.{}: starting...".format(i)) test_node.last_message.pop("getdata", None) blocks = [] # Create two more blocks. for _ in range(2): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 # Send the header of the second block -> this won't connect. with p2p_lock: test_node.last_message.pop("getheaders", None) test_node.send_header_for_blocks([blocks[1]]) test_node.wait_for_getheaders() test_node.send_header_for_blocks(blocks) test_node.wait_for_getdata([x.sha256 for x in blocks]) [test_node.send_message(msg_block(x)) for x in blocks] test_node.sync_with_ping() assert_equal( int(self.nodes[0].getbestblockhash(), 16), blocks[1].sha256) blocks = [] # Now we test that if we repeatedly don't send connecting headers, we # don't go into an infinite loop trying to get them to connect. MAX_UNCONNECTING_HEADERS = 10 for _ in range(MAX_UNCONNECTING_HEADERS + 1): blocks.append(create_block( tip, create_coinbase(height), block_time)) blocks[-1].solve() tip = blocks[-1].sha256 block_time += 1 height += 1 for i in range(1, MAX_UNCONNECTING_HEADERS): # Send a header that doesn't connect, check that we get a # getheaders. with p2p_lock: test_node.last_message.pop("getheaders", None) test_node.send_header_for_blocks([blocks[i]]) test_node.wait_for_getheaders() # Next header will connect, should re-set our count: test_node.send_header_for_blocks([blocks[0]]) # Remove the first two entries (blocks[1] would connect): blocks = blocks[2:] # Now try to see how many unconnecting headers we can send # before we get disconnected. Should be 5*MAX_UNCONNECTING_HEADERS for i in range(5 * MAX_UNCONNECTING_HEADERS - 1): # Send a header that doesn't connect, check that we get a # getheaders. with p2p_lock: test_node.last_message.pop("getheaders", None) test_node.send_header_for_blocks([blocks[i % len(blocks)]]) test_node.wait_for_getheaders() # Eventually this stops working. test_node.send_header_for_blocks([blocks[-1]]) # Should get disconnected test_node.wait_for_disconnect() self.log.info("Part 5: success!") # Finally, check that the inv node never received a getdata request, # throughout the test assert "getdata" not in inv_node.last_message if __name__ == '__main__': SendHeadersTest().main() diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index ee1021f31..f9643c944 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -1,202 +1,200 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test multisig RPCs""" import decimal import itertools import json import os from test_framework.authproxy import JSONRPCException from test_framework.descriptors import descsum_create, drop_origins from test_framework.key import ECKey, ECPubKey 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 RpcCreateMultiSigTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def get_keys(self): self.pub = [] self.priv = [] node0, node1, node2 = self.nodes for _ in range(self.nkeys): k = ECKey() k.generate() self.pub.append(k.get_pubkey().get_bytes().hex()) self.priv.append(bytes_to_wif(k.get_bytes(), k.is_compressed)) self.final = node2.getnewaddress() def run_test(self): node0, node1, node2 = self.nodes self.check_addmultisigaddress_errors() self.log.info('Generating blocks ...') self.generate(node0, 149) - self.sync_all() self.moved = 0 for self.nkeys in [3, 5]: for self.nsigs in [2, 3]: self.get_keys() self.do_multisig() self.checkbalances() # Test mixed compressed and uncompressed pubkeys self.log.info( 'Mixed compressed and uncompressed multisigs are not allowed') pk0 = node0.getaddressinfo(node0.getnewaddress())['pubkey'] pk1 = node1.getaddressinfo(node1.getnewaddress())['pubkey'] pk2 = node2.getaddressinfo(node2.getnewaddress())['pubkey'] # decompress pk2 pk_obj = ECPubKey() pk_obj.set(bytes.fromhex(pk2)) pk_obj.compressed = False pk2 = pk_obj.get_bytes().hex() node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) wmulti0 = node0.get_wallet_rpc('wmulti0') # Check all permutations of keys because order matters apparently for keys in itertools.permutations([pk0, pk1, pk2]): # Results should be the same as this legacy one legacy_addr = node0.createmultisig(2, keys)['address'] assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '')['address']) self.log.info( 'Testing sortedmulti descriptors with BIP 67 test vectors') with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f: vectors = json.load(f) for t in vectors: key_str = ','.join(t['keys']) desc = descsum_create('sh(sortedmulti(2,{}))'.format(key_str)) assert_equal(self.nodes[0].deriveaddresses(desc)[0], t['address']) sorted_key_str = ','.join(t['sorted_keys']) sorted_key_desc = descsum_create( 'sh(multi(2,{}))'.format(sorted_key_str)) assert_equal(self.nodes[0].deriveaddresses( sorted_key_desc)[0], t['address']) def check_addmultisigaddress_errors(self): if self.options.descriptors: return self.log.info( 'Check that addmultisigaddress fails when the private keys are missing') addresses = [self.nodes[1].getnewaddress( address_type='legacy') for _ in range(2)] assert_raises_rpc_error(-5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses)) for a in addresses: # Importing all addresses should not change the result self.nodes[0].importaddress(a) assert_raises_rpc_error(-5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses)) def checkbalances(self): node0, node1, node2 = self.nodes self.generate(node0, 100) - self.sync_all() bal0 = node0.getbalance() bal1 = node1.getbalance() bal2 = node2.getbalance() height = node0.getblockchaininfo()["blocks"] assert 150 < height < 350 total = 149 * 50000000 + (height - 149 - 100) * 25000000 assert bal1 == 0 assert bal2 == self.moved assert bal0 + bal1 + bal2 == total def do_multisig(self): node0, node1, node2 = self.nodes if 'wmulti' not in node1.listwallets(): try: node1.loadwallet('wmulti') except JSONRPCException as e: path = os.path.join(self.options.tmpdir, "node1", "regtest", "wallets", "wmulti") if e.error['code'] == -18 and ( "Wallet file verification failed. Failed to load " "database path '{}'. Path does not exist.".format(path) in e.error['message']): node1.createwallet(wallet_name='wmulti', disable_private_keys=True) else: raise wmulti = node1.get_wallet_rpc('wmulti') # Construct the expected descriptor desc = 'multi({},{})'.format(self.nsigs, ','.join(self.pub)) desc = 'sh({})'.format(desc) desc = descsum_create(desc) msig = node2.createmultisig(self.nsigs, self.pub) madd = msig["address"] mredeem = msig["redeemScript"] assert_equal(desc, msig['descriptor']) # compare against addmultisigaddress msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None) maddw = msigw["address"] mredeemw = msigw["redeemScript"] assert_equal(desc, drop_origins(msigw['descriptor'])) # addmultisigiaddress and createmultisig work the same assert maddw == madd assert mredeemw == mredeem txid = node0.sendtoaddress(madd, 40000000) tx = node0.getrawtransaction(txid, True) vout = [v["n"] for v in tx["vout"] if madd in v["scriptPubKey"].get("addresses", [])] assert len(vout) == 1 vout = vout[0] scriptPubKey = tx["vout"][vout]["scriptPubKey"]["hex"] value = tx["vout"][vout]["value"] prevtxs = [{"txid": txid, "vout": vout, "scriptPubKey": scriptPubKey, "redeemScript": mredeem, "amount": value}] self.generate(node0, 1) outval = value - decimal.Decimal("10.00") rawtx = node2.createrawtransaction( [{"txid": txid, "vout": vout}], [{self.final: outval}]) rawtx2 = node2.signrawtransactionwithkey( rawtx, self.priv[0:self.nsigs - 1], prevtxs) rawtx3 = node2.signrawtransactionwithkey( rawtx2["hex"], [self.priv[-1]], prevtxs) self.moved += outval tx = node0.sendrawtransaction(rawtx3["hex"], 0) blk = self.generate(node0, 1)[0] assert tx in node0.getblock(blk)["tx"] txinfo = node0.getrawtransaction(tx, True, blk) self.log.info("n/m={}/{} size={}".format(self.nsigs, self.nkeys, txinfo["size"])) wmulti.unloadwallet() if __name__ == '__main__': RpcCreateMultiSigTest().main() diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 076b59807..b81c251d8 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -1,922 +1,910 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. from decimal import Decimal from test_framework.descriptors import descsum_create from test_framework.messages import CTransaction, FromHex from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_fee_amount, assert_greater_than, assert_greater_than_or_equal, assert_raises_rpc_error, find_vout_for_address, ) def get_unspent(listunspent, amount): for utx in listunspent: if utx['amount'] == amount: return utx raise AssertionError( 'Could not find unspent with amount={}'.format(amount)) class RawTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True # This test isn't testing tx relay. Set whitelist on the peers for # instant tx relay. self.extra_args = [['-whitelist=noban@127.0.0.1']] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): self.setup_nodes() 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") self.min_relay_tx_fee = self.nodes[0].getnetworkinfo()['relayfee'] # This test is not meant to test fee estimation and we'd like # to be sure all txs are sent at a consistent desired feerate for node in self.nodes: node.settxfee(self.min_relay_tx_fee) # if the fee's positive delta is higher than this value tests will fail, # neg. delta always fail the tests. # The size of the signature of every input may be at most 2 bytes larger # than a minimum sized signature. # = 2 bytes * minRelayTxFeePerByte self.fee_tolerance = 2 * self.min_relay_tx_fee / 1000 self.generate(self.nodes[2], 1) - self.sync_all() self.generate(self.nodes[0], 121) - self.sync_all() self.test_change_position() self.test_simple() self.test_simple_two_coins() self.test_simple_two_outputs() self.test_change() self.test_no_change() self.test_invalid_option() self.test_invalid_change_address() self.test_valid_change_address() self.test_coin_selection() self.test_two_vin() self.test_two_vin_two_vout() self.test_invalid_input() self.test_fee_p2pkh() self.test_fee_p2pkh_multi_out() self.test_fee_p2sh() self.test_fee_4of5() self.test_spend_2of2() self.test_locked_wallet() self.test_many_inputs_fee() self.test_many_inputs_send() self.test_op_return() self.test_watchonly() self.test_all_watched_funds() self.test_option_feerate() self.test_address_reuse() self.test_option_subtract_fee_from_outputs() self.test_subtract_fee_with_presets() def test_change_position(self): """Ensure setting changePosition in fundraw with an exact match is handled properly.""" self.log.info("Test fundrawtxn changePosition option") rawmatch = self.nodes[2].createrawtransaction( [], {self.nodes[2].getnewaddress(): 50000000}) 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(200000000) 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].get_wallet_rpc( self.default_wallet_name).getnewaddress(), self.watchonly_amount / 10) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1500000) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1000000) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 5000000) self.generate(self.nodes[0], 1) - self.sync_all() wwatch.unloadwallet() def test_simple(self): self.log.info("Test fundrawtxn") inputs = [] outputs = {self.nodes[0].getnewaddress(): 1000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) # test that we have enough inputs assert len(dec_tx['vin']) > 0 def test_simple_two_coins(self): self.log.info("Test fundrawtxn with 2 coins") inputs = [] outputs = {self.nodes[0].getnewaddress(): 2200000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) # test if we have enough inputs assert len(dec_tx['vin']) > 0 assert_equal(dec_tx['vin'][0]['scriptSig']['hex'], '') def test_simple_two_outputs(self): self.log.info("Test fundrawtxn with 2 outputs") inputs = [] outputs = { self.nodes[0].getnewaddress(): 2600000, self.nodes[1].getnewaddress(): 2500000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 for out in dec_tx['vout']: totalOut += out['value'] assert len(dec_tx['vin']) > 0 assert_equal(dec_tx['vin'][0]['scriptSig']['hex'], '') def test_change(self): self.log.info("Test fundrawtxn with a vin > required amount") utx = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = {self.nodes[0].getnewaddress(): 1000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) fee = rawtxfund['fee'] # Use the same fee for the next tx self.test_no_change_fee = fee dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 for out in dec_tx['vout']: totalOut += out['value'] # compare vin total and totalout+fee assert_equal(fee + totalOut, utx['amount']) def test_no_change(self): self.log.info("Test fundrawtxn not having a change output") utx = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = { self.nodes[0].getnewaddress(): Decimal(5000000) - self.test_no_change_fee - self.fee_tolerance} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) fee = rawtxfund['fee'] dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 for out in dec_tx['vout']: totalOut += out['value'] assert_equal(rawtxfund['changepos'], -1) # compare vin total and totalout+fee assert_equal(fee + totalOut, utx['amount']) def test_invalid_option(self): self.log.info("Test fundrawtxn with an invalid option") utx = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = {self.nodes[0].getnewaddress(): Decimal(4000000)} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) assert_raises_rpc_error(-3, "Unexpected key foo", self.nodes[ 2].fundrawtransaction, rawTx, {'foo': 'bar'}) # reserveChangeKey was deprecated and is now removed assert_raises_rpc_error(-3, "Unexpected key reserveChangeKey", lambda: self.nodes[2].fundrawtransaction(hexstring=rawTx, options={'reserveChangeKey': True})) def test_invalid_change_address(self): self.log.info("Test fundrawtxn with an invalid change address") utx = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = {self.nodes[0].getnewaddress(): Decimal(4000000)} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) assert_raises_rpc_error( -5, "Change address must be a valid bitcoin address", self.nodes[2].fundrawtransaction, rawTx, {'changeAddress': 'foobar'}) def test_valid_change_address(self): self.log.info("Test fundrawtxn with a provided change address") utx = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = {self.nodes[0].getnewaddress(): Decimal(4000000)} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) change = self.nodes[2].getnewaddress() assert_raises_rpc_error(-8, "changePosition out of bounds", self.nodes[ 2].fundrawtransaction, rawTx, {'changeAddress': change, 'changePosition': 2}) rawtxfund = self.nodes[2].fundrawtransaction( rawTx, {'changeAddress': change, 'changePosition': 0}) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) out = dec_tx['vout'][0] assert_equal(change, out['scriptPubKey']['addresses'][0]) def test_coin_selection(self): self.log.info("Test fundrawtxn with a vin < required amount") utx = get_unspent(self.nodes[2].listunspent(), 1000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}] outputs = {self.nodes[0].getnewaddress(): 1000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) # 4-byte version + 1-byte vin count + 36-byte prevout then script_len rawTx = rawTx[:82] + "0100" + rawTx[84:] dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex']) # Should fail without add_inputs: assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawTx, {"add_inputs": False}) # add_inputs is enabled by default rawtxfund = self.nodes[2].fundrawtransaction(rawTx) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 matchingOuts = 0 for i, out in enumerate(dec_tx['vout']): totalOut += out['value'] if out['scriptPubKey']['addresses'][0] in outputs: matchingOuts += 1 else: assert_equal(i, rawtxfund['changepos']) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex']) assert_equal(matchingOuts, 1) assert_equal(len(dec_tx['vout']), 2) def test_two_vin(self): self.log.info("Test fundrawtxn with 2 vins") utx = get_unspent(self.nodes[2].listunspent(), 1000000) utx2 = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}, {'txid': utx2['txid'], 'vout': utx2['vout']}] outputs = {self.nodes[0].getnewaddress(): 6000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) # Should fail without add_inputs: assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawTx, {"add_inputs": False}) rawtxfund = self.nodes[2].fundrawtransaction( rawTx, {"add_inputs": True}) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 matchingOuts = 0 for out in dec_tx['vout']: totalOut += out['value'] if out['scriptPubKey']['addresses'][0] in outputs: matchingOuts += 1 assert_equal(matchingOuts, 1) assert_equal(len(dec_tx['vout']), 2) matchingIns = 0 for vinOut in dec_tx['vin']: for vinIn in inputs: if vinIn['txid'] == vinOut['txid']: matchingIns += 1 # we now must see two vins identical to vins given as params assert_equal(matchingIns, 2) def test_two_vin_two_vout(self): self.log.info("Test fundrawtxn with 2 vins and 2 vouts") utx = get_unspent(self.nodes[2].listunspent(), 1000000) utx2 = get_unspent(self.nodes[2].listunspent(), 5000000) inputs = [{'txid': utx['txid'], 'vout': utx['vout']}, {'txid': utx2['txid'], 'vout': utx2['vout']}] outputs = { self.nodes[0].getnewaddress(): 6000000, self.nodes[0].getnewaddress(): 1000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(utx['txid'], dec_tx['vin'][0]['txid']) # Should fail without add_inputs: assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawTx, {"add_inputs": False}) rawtxfund = self.nodes[2].fundrawtransaction( rawTx, {"add_inputs": True}) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) totalOut = 0 matchingOuts = 0 for out in dec_tx['vout']: totalOut += out['value'] if out['scriptPubKey']['addresses'][0] in outputs: matchingOuts += 1 assert_equal(matchingOuts, 2) assert_equal(len(dec_tx['vout']), 3) def test_invalid_input(self): self.log.info("Test fundrawtxn with an invalid vin") inputs = [ {'txid': "1c7f966dab21119bac53213a2bc7532bff1fa844c124fd750a7d0b1332440bd1", 'vout': 0}] outputs = {self.nodes[0].getnewaddress(): 1000000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) assert_raises_rpc_error( -4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawTx) def test_fee_p2pkh(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn p2pkh fee") inputs = [] outputs = {self.nodes[1].getnewaddress(): 1100000} rawTx = self.nodes[0].createrawtransaction(inputs, outputs) fundedTx = self.nodes[0].fundrawtransaction(rawTx) # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress( self.nodes[1].getnewaddress(), 1100000) signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance def test_fee_p2pkh_multi_out(self): """Compare fee of a standard pubkeyhash transaction with multiple outputs.""" self.log.info("Test fundrawtxn p2pkh fee with multiple outputs") inputs = [] outputs = { self.nodes[1].getnewaddress(): 1100000, self.nodes[1].getnewaddress(): 1200000, self.nodes[1].getnewaddress(): 100000, self.nodes[1].getnewaddress(): 1300000, self.nodes[1].getnewaddress(): 200000, self.nodes[1].getnewaddress(): 300000, } rawtx = self.nodes[0].createrawtransaction(inputs, outputs) fundedTx = self.nodes[0].fundrawtransaction(rawtx) # Create same transaction over sendtoaddress. txId = self.nodes[0].sendmany("", outputs) signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance def test_fee_p2sh(self): """Compare fee of a 2-of-2 multisig p2sh transaction.""" # Create 2-of-2 addr. addr1 = self.nodes[1].getnewaddress() addr2 = self.nodes[1].getnewaddress() addr1Obj = self.nodes[1].getaddressinfo(addr1) addr2Obj = self.nodes[1].getaddressinfo(addr2) mSigObj = self.nodes[3].createmultisig( 2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] inputs = [] outputs = {mSigObj: 1100000} rawTx = self.nodes[0].createrawtransaction(inputs, outputs) fundedTx = self.nodes[0].fundrawtransaction(rawTx) # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1100000) signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance def test_fee_4of5(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn fee with 4-of-5 addresses") # Create 4-of-5 addr. addr1 = self.nodes[1].getnewaddress() addr2 = self.nodes[1].getnewaddress() addr3 = self.nodes[1].getnewaddress() addr4 = self.nodes[1].getnewaddress() addr5 = self.nodes[1].getnewaddress() addr1Obj = self.nodes[1].getaddressinfo(addr1) addr2Obj = self.nodes[1].getaddressinfo(addr2) addr3Obj = self.nodes[1].getaddressinfo(addr3) addr4Obj = self.nodes[1].getaddressinfo(addr4) addr5Obj = self.nodes[1].getaddressinfo(addr5) mSigObj = self.nodes[1].createmultisig( 4, [ addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey'], addr4Obj['pubkey'], addr5Obj['pubkey'], ] )['address'] inputs = [] outputs = {mSigObj: 1100000} rawTx = self.nodes[0].createrawtransaction(inputs, outputs) fundedTx = self.nodes[0].fundrawtransaction(rawTx) # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1100000) signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance def test_spend_2of2(self): """Spend a 2-of-2 multisig transaction over fundraw.""" self.log.info("Test fundpsbt spending 2-of-2 multisig") # Create 2-of-2 addr. addr1 = self.nodes[2].getnewaddress() addr2 = self.nodes[2].getnewaddress() addr1Obj = self.nodes[2].getaddressinfo(addr1) addr2Obj = self.nodes[2].getaddressinfo(addr2) 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,200,000 XEC to msig addr. self.nodes[0].sendtoaddress(mSigObj, 1200000) self.generate(self.nodes[0], 1) - self.sync_all() oldBalance = self.nodes[1].getbalance() inputs = [] outputs = {self.nodes[1].getnewaddress(): 1100000} funded_psbt = wmulti.walletcreatefundedpsbt( inputs=inputs, outputs=outputs, options={'changeAddress': w2.getrawchangeaddress()})['psbt'] signed_psbt = w2.walletprocesspsbt(funded_psbt) final_psbt = w2.finalizepsbt(signed_psbt['psbt']) self.nodes[2].sendrawtransaction(final_psbt['hex']) self.generate(self.nodes[2], 1) - self.sync_all() # Make sure funds are received at node1. assert_equal( oldBalance + Decimal('1100000.00'), self.nodes[1].getbalance()) wmulti.unloadwallet() def test_locked_wallet(self): 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('pkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'), 'timestamp': 'now', 'active': True }, { 'desc': descsum_create('pkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'), 'timestamp': 'now', 'active': True, 'internal': True }]) self.nodes[1].walletlock() # Drain the keypool. self.nodes[1].getnewaddress() self.nodes[1].getrawchangeaddress() inputs = [] outputs = {self.nodes[0].getnewaddress(): 1099997.00} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) # fund a transaction that does not require a new key for the change # output self.nodes[1].fundrawtransaction(rawtx) # fund a transaction that requires a new key for the change output # creating the key must be impossible because the wallet is locked outputs = {self.nodes[0].getnewaddress(): 1100000} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) assert_raises_rpc_error( -4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", self.nodes[1].fundrawtransaction, rawtx) # Refill the keypool. self.nodes[1].walletpassphrase("test", 100) # need to refill the keypool to get an internal change address self.nodes[1].keypoolrefill(8) self.nodes[1].walletlock() assert_raises_rpc_error(-13, "walletpassphrase", self.nodes[ 1].sendtoaddress, self.nodes[0].getnewaddress(), 1200000) oldBalance = self.nodes[0].getbalance() inputs = [] outputs = {self.nodes[0].getnewaddress(): 1100000} rawTx = self.nodes[1].createrawtransaction(inputs, outputs) fundedTx = self.nodes[1].fundrawtransaction(rawTx) # Now we need to unlock. self.nodes[1].walletpassphrase("test", 600) signedTx = self.nodes[1].signrawtransactionwithwallet(fundedTx['hex']) self.nodes[1].sendrawtransaction(signedTx['hex']) self.generate(self.nodes[1], 1) - self.sync_all() # Make sure funds are received at node1. assert_equal( oldBalance + Decimal('51100000.00'), self.nodes[0].getbalance()) def test_many_inputs_fee(self): """Multiple (~19) inputs tx test | Compare fee.""" self.log.info("Test fundrawtxn fee with many inputs") # Empty node1, send some small coins from node0 to node1. self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) self.generate(self.nodes[1], 1) - self.sync_all() for _ in range(20): self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 10000) self.generate(self.nodes[0], 1) - self.sync_all() # Fund a tx with ~20 small inputs. inputs = [] outputs = { self.nodes[0].getnewaddress(): 150000, self.nodes[0].getnewaddress(): 40000} rawTx = self.nodes[1].createrawtransaction(inputs, outputs) fundedTx = self.nodes[1].fundrawtransaction(rawTx) # Create same transaction over sendtoaddress. txId = self.nodes[1].sendmany("", outputs) signedFee = self.nodes[1].getrawmempool(True)[txId]['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) # ~19 inputs assert feeDelta >= 0 and feeDelta <= self.fee_tolerance * 19 def test_many_inputs_send(self): """Multiple (~19) inputs tx test | sign/send.""" self.log.info("Test fundrawtxn sign+send with many inputs") # Again, empty node1, send some small coins from node0 to node1. self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) self.generate(self.nodes[1], 1) - self.sync_all() for _ in range(20): self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 10000) self.generate(self.nodes[0], 1) - self.sync_all() # Fund a tx with ~20 small inputs. oldBalance = self.nodes[0].getbalance() inputs = [] outputs = { self.nodes[0].getnewaddress(): 150000, self.nodes[0].getnewaddress(): 40000} rawTx = self.nodes[1].createrawtransaction(inputs, outputs) fundedTx = self.nodes[1].fundrawtransaction(rawTx) fundedAndSignedTx = self.nodes[1].signrawtransactionwithwallet( fundedTx['hex']) self.nodes[1].sendrawtransaction(fundedAndSignedTx['hex']) self.generate(self.nodes[1], 1) - self.sync_all() assert_equal(oldBalance + Decimal('50190000.00'), self.nodes[0].getbalance()) # 0.19+block reward def test_op_return(self): self.log.info("Test fundrawtxn with OP_RETURN and no vin") rawTx = "0100000000010000000000000000066a047465737400000000" dec_tx = self.nodes[2].decoderawtransaction(rawTx) assert_equal(len(dec_tx['vin']), 0) assert_equal(len(dec_tx['vout']), 1) rawtxfund = self.nodes[2].fundrawtransaction(rawTx) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) # at least one vin assert_greater_than(len(dec_tx['vin']), 0) # one change output added assert_equal(len(dec_tx['vout']), 2) def test_watchonly(self): self.log.info("Test fundrawtxn using only watchonly") inputs = [] outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount / 2} rawTx = self.nodes[3].createrawtransaction(inputs, outputs) 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( "pkh(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) 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") inputs = [] outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount} rawTx = self.nodes[3].createrawtransaction(inputs, outputs) 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"]), 1) assert res_dec["vin"][0]["txid"] == self.watchonly_txid assert_greater_than(result["fee"], 0) assert_equal(result["changepos"], -1) assert_equal(result["fee"] + res_dec["vout"][0]["value"], self.watchonly_amount) signedtx = wwatch.signrawtransactionwithwallet(result["hex"]) assert not signedtx["complete"] signedtx = self.nodes[0].signrawtransactionwithwallet(signedtx["hex"]) assert signedtx["complete"] self.nodes[0].sendrawtransaction(signedtx["hex"]) self.generate(self.nodes[0], 1) - self.sync_all() wwatch.unloadwallet() def test_option_feerate(self): self.log.info("Test fundrawtxn feeRate option") # 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(): 1000000} rawTx = self.nodes[3].createrawtransaction(inputs, outputs) # uses self.min_relay_tx_fee (set by settxfee) result = self.nodes[3].fundrawtransaction(rawTx) result2 = self.nodes[3].fundrawtransaction( rawTx, {"feeRate": 2 * self.min_relay_tx_fee}) result_fee_rate = result['fee'] * 1000 / \ FromHex(CTransaction(), result['hex']).billable_size() assert_fee_amount( result2['fee'], FromHex(CTransaction(), result2['hex']).billable_size(), 2 * result_fee_rate) result3 = self.nodes[3].fundrawtransaction( rawTx, {"feeRate": 10 * self.min_relay_tx_fee}) assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[3].fundrawtransaction, rawTx, {"feeRate": 1000000}) # allow this transaction to be underfunded by 10 bytes. This is due # to the first transaction possibly being overfunded by up to .9 # satoshi due to fee ceilings being used. assert_fee_amount( result3['fee'], FromHex(CTransaction(), result3['hex']).billable_size(), 10 * result_fee_rate, 10) def test_address_reuse(self): """Test no address reuse occurs.""" self.log.info("Test fundrawtxn does not reuse addresses") rawTx = self.nodes[3].createrawtransaction( inputs=[], outputs={self.nodes[3].getnewaddress(): 1000000}) result3 = self.nodes[3].fundrawtransaction(rawTx) res_dec = self.nodes[0].decoderawtransaction(result3["hex"]) changeaddress = "" for out in res_dec['vout']: if out['value'] > 1000000.0: changeaddress += out['scriptPubKey']['addresses'][0] assert changeaddress != "" nextaddr = self.nodes[3].getnewaddress() # Now the change address key should be removed from the keypool. assert changeaddress != nextaddr def test_option_subtract_fee_from_outputs(self): self.log.info("Test fundrawtxn subtractFeeFromOutputs option") # 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[2].getnewaddress(): 1000000} rawTx = self.nodes[3].createrawtransaction(inputs, outputs) # uses self.min_relay_tx_fee (set by settxfee) result = [self.nodes[3].fundrawtransaction(rawTx), # empty subtraction list self.nodes[3].fundrawtransaction( rawTx, {"subtractFeeFromOutputs": []}), # uses self.min_relay_tx_fee (set by settxfee) self.nodes[3].fundrawtransaction( rawTx, {"subtractFeeFromOutputs": [0]}), 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]) inputs = [] outputs = { self.nodes[2].getnewaddress(): value for value in (1000000.0, 1100000.0, 1200000.0, 1300000.0)} rawTx = self.nodes[3].createrawtransaction(inputs, outputs) # Split the fee between outputs 0, 2, and 3, but not output 1 result = [self.nodes[3].fundrawtransaction(rawTx), self.nodes[3].fundrawtransaction(rawTx, {"subtractFeeFromOutputs": [0, 2, 3]})] dec_tx = [self.nodes[3].decoderawtransaction(result[0]['hex']), self.nodes[3].decoderawtransaction(result[1]['hex'])] # Nested list of non-change output amounts for each transaction. output = [[out['value'] for i, out in enumerate(d['vout']) if i != r['changepos']] for d, r in zip(dec_tx, result)] # List of differences in output amounts between normal and subtractFee # transactions. share = [o0 - o1 for o0, o1 in zip(output[0], output[1])] # Output 1 is the same in both transactions. assert_equal(share[1], 0) # The other 3 outputs are smaller as a result of # subtractFeeFromOutputs. assert_greater_than(share[0], 0) assert_greater_than(share[2], 0) assert_greater_than(share[3], 0) # Outputs 2 and 3 take the same share of the fee. assert_equal(share[2], share[3]) # Output 0 takes at least as much share of the fee, and no more than 2 # satoshis more, than outputs 2 and 3. assert_greater_than_or_equal(share[0], share[2]) assert_greater_than_or_equal(share[2] + Decimal("2e-8"), share[0]) # The fee is the same in both transactions. assert_equal(result[0]['fee'], result[1]['fee']) # The total subtracted from the outputs is equal to the fee. assert_equal(share[0] + share[2] + share[3], result[0]['fee']) def test_subtract_fee_with_presets(self): self.log.info( "Test fundrawtxn subtract fee from outputs with preset inputs that are sufficient") addr = self.nodes[0].getnewaddress() txid = self.nodes[0].sendtoaddress(addr, 10000000) vout = find_vout_for_address(self.nodes[0], txid, addr) rawtx = self.nodes[0].createrawtransaction([{'txid': txid, 'vout': vout}], [ {self.nodes[0].getnewaddress(): 5000000}]) fundedtx = self.nodes[0].fundrawtransaction( rawtx, {'subtractFeeFromOutputs': [0]}) signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx['hex']) self.nodes[0].sendrawtransaction(signedtx['hex']) if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_getblockstats.py b/test/functional/rpc_getblockstats.py index ecbb51795..8708414b4 100755 --- a/test/functional/rpc_getblockstats.py +++ b/test/functional/rpc_getblockstats.py @@ -1,197 +1,196 @@ #!/usr/bin/env python3 # Copyright (c) 2017-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Test getblockstats rpc call # import decimal import json import os from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error TESTSDIR = os.path.dirname(os.path.realpath(__file__)) def EncodeDecimal(o): if isinstance(o, decimal.Decimal): # json.load will read a quoted float as a string and not convert it back # to decimal, so store the value as unquoted float instead. return float(o) raise TypeError(repr(o) + " is not JSON serializable") class GetblockstatsTest(BitcoinTestFramework): start_height = 101 max_stat_pos = 2 def add_options(self, parser): parser.add_argument('--gen-test-data', dest='gen_test_data', default=False, action='store_true', help='Generate test data') parser.add_argument('--test-data', dest='test_data', default='data/rpc_getblockstats.json', action='store', metavar='FILE', help='Test data file') def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.supports_cli = False def get_stats(self): return [self.nodes[0].getblockstats( hash_or_height=self.start_height + i) for i in range(self.max_stat_pos + 1)] def generate_test_data(self, filename): mocktime = 1525107225 self.nodes[0].setmocktime(mocktime) self.generate(self.nodes[0], 101) address = self.nodes[0].get_deterministic_priv_key().address self.nodes[0].sendtoaddress( address=address, amount=10000000, subtractfeefromamount=True) self.generate(self.nodes[0], 1) - self.sync_all() self.nodes[0].sendtoaddress( address=address, amount=10000000, subtractfeefromamount=True) self.nodes[0].sendtoaddress( address=address, amount=10000000, subtractfeefromamount=False) self.nodes[0].settxfee(amount=3000) self.nodes[0].sendtoaddress( address=address, amount=1000000, subtractfeefromamount=True) self.sync_all() self.generate(self.nodes[0], 1) self.expected_stats = self.get_stats() blocks = [] tip = self.nodes[0].getbestblockhash() blockhash = None height = 0 while tip != blockhash: blockhash = self.nodes[0].getblockhash(height) blocks.append(self.nodes[0].getblock(blockhash, 0)) height += 1 to_dump = { 'blocks': blocks, 'mocktime': int(mocktime), 'stats': self.expected_stats, } with open(filename, 'w', encoding="utf8") as f: json.dump(to_dump, f, sort_keys=True, indent=2, default=EncodeDecimal) def load_test_data(self, filename): with open(filename, 'r', encoding="utf8") as f: d = json.load(f, parse_float=decimal.Decimal) blocks = d['blocks'] mocktime = d['mocktime'] self.expected_stats = d['stats'] self.log.info(self.expected_stats) # Set the timestamps from the file so that the nodes can get out of # Initial Block Download self.nodes[0].setmocktime(mocktime) self.sync_all() for i, b in enumerate(blocks): self.nodes[0].submitblock(b) def run_test(self): test_data = os.path.join(TESTSDIR, self.options.test_data) if self.options.gen_test_data: self.generate_test_data(test_data) else: self.load_test_data(test_data) self.sync_all() stats = self.get_stats() # Make sure all valid statistics are included but nothing else is expected_keys = self.expected_stats[0].keys() assert_equal(set(stats[0].keys()), set(expected_keys)) assert_equal(stats[0]['height'], self.start_height) assert_equal(stats[self.max_stat_pos]['height'], self.start_height + self.max_stat_pos) for i in range(self.max_stat_pos + 1): self.log.info('Checking block {}\n'.format(i)) assert_equal(stats[i], self.expected_stats[i]) # Check selecting block by hash too blockhash = self.expected_stats[i]['blockhash'] stats_by_hash = self.nodes[0].getblockstats( hash_or_height=blockhash) assert_equal(stats_by_hash, self.expected_stats[i]) # Make sure each stat can be queried on its own for stat in expected_keys: for i in range(self.max_stat_pos + 1): result = self.nodes[0].getblockstats( hash_or_height=self.start_height + i, stats=[stat]) assert_equal(list(result.keys()), [stat]) if result[stat] != self.expected_stats[i][stat]: self.log.info('result[{}] ({}) failed, {!r} != {!r}'.format( stat, i, result[stat], self.expected_stats[i][stat])) assert_equal(result[stat], self.expected_stats[i][stat]) # Make sure only the selected statistics are included (more than one) some_stats = {'minfee', 'maxfee'} stats = self.nodes[0].getblockstats( hash_or_height=1, stats=list(some_stats)) assert_equal(set(stats.keys()), some_stats) # Test invalid parameters raise the proper json exceptions tip = self.start_height + self.max_stat_pos assert_raises_rpc_error(-8, 'Target block height {} after current tip {}'.format( tip + 1, tip), self.nodes[0].getblockstats, hash_or_height=tip + 1) assert_raises_rpc_error(-8, 'Target block height {} is negative'.format(-1), self.nodes[0].getblockstats, hash_or_height=-1) # Make sure not valid stats aren't allowed inv_sel_stat = 'asdfghjkl' inv_stats = [ [inv_sel_stat], ['minfee', inv_sel_stat], [inv_sel_stat, 'minfee'], ['minfee', inv_sel_stat, 'maxfee'], ] for inv_stat in inv_stats: assert_raises_rpc_error(-8, 'Invalid selected statistic {}'.format( inv_sel_stat), self.nodes[0].getblockstats, hash_or_height=1, stats=inv_stat) # Make sure we aren't always returning inv_sel_stat as the culprit stat # Mainchain's genesis block shouldn't be found on regtest assert_raises_rpc_error(-5, 'Block not found', self.nodes[0].getblockstats, hash_or_height='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f') # Invalid number of args assert_raises_rpc_error(-1, 'getblockstats hash_or_height ( stats )', self.nodes[0].getblockstats, '00', 1, 2) assert_raises_rpc_error(-1, 'getblockstats hash_or_height ( stats )', self.nodes[0].getblockstats) if __name__ == '__main__': GetblockstatsTest().main() diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 0dae84494..26ffb03b2 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -1,468 +1,463 @@ #!/usr/bin/env python3 # Copyright (c) 2018 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 the Partially Signed Transaction RPCs. """ import json import os from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, assert_equal, assert_raises_rpc_error, find_output, ) # Create one-input, one-output, no-fee transaction: class PSBTTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Create and fund a raw tx for sending 10 BTC psbtx1 = self.nodes[0].walletcreatefundedpsbt( [], {self.nodes[2].getnewaddress(): 10000000})['psbt'] # If inputs are specified, do not automatically add more: utxo1 = self.nodes[0].listunspent()[0] assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress(): 90000000}) psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], { self.nodes[2].getnewaddress(): 90000000}, 0, {"add_inputs": True})['psbt'] assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2) # Inputs argument can be null self.nodes[0].walletcreatefundedpsbt( None, {self.nodes[2].getnewaddress(): 10}) # Node 1 should not be able to add anything to it but still return the # psbtx same as before psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt'] assert_equal(psbtx1, psbtx) # Sign the transaction and send signed_tx = self.nodes[0].walletprocesspsbt(psbtx)['psbt'] final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex'] self.nodes[0].sendrawtransaction(final_tx) # Manually selected inputs can be locked: assert_equal(len(self.nodes[0].listlockunspent()), 0) utxo1 = self.nodes[0].listunspent()[0] psbtx1 = self.nodes[0].walletcreatefundedpsbt( [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress(): 1_000_000}, 0, {"lockUnspents": True})["psbt"] assert_equal(len(self.nodes[0].listlockunspent()), 1) # Locks are ignored for manually selected inputs self.nodes[0].walletcreatefundedpsbt( [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress(): 1_000_000}, 0) # Create p2sh, p2pkh addresses pubkey0 = self.nodes[0].getaddressinfo( self.nodes[0].getnewaddress())['pubkey'] pubkey1 = self.nodes[1].getaddressinfo( self.nodes[1].getnewaddress())['pubkey'] pubkey2 = self.nodes[2].getaddressinfo( self.nodes[2].getnewaddress())['pubkey'] p2sh = self.nodes[1].addmultisigaddress( 2, [pubkey0, pubkey1, pubkey2], "")['address'] p2pkh = self.nodes[1].getnewaddress("") # fund those addresses rawtx = self.nodes[0].createrawtransaction( [], {p2sh: 10000000, p2pkh: 10000000}) rawtx = self.nodes[0].fundrawtransaction(rawtx, {"changePosition": 0}) signed_tx = self.nodes[0].signrawtransactionwithwallet(rawtx['hex'])[ 'hex'] txid = self.nodes[0].sendrawtransaction(signed_tx) self.generate(self.nodes[0], 6) - self.sync_all() # Find the output pos p2sh_pos = -1 p2pkh_pos = -1 decoded = self.nodes[0].decoderawtransaction(signed_tx) for out in decoded['vout']: if out['scriptPubKey']['addresses'][0] == p2sh: p2sh_pos = out['n'] elif out['scriptPubKey']['addresses'][0] == p2pkh: p2pkh_pos = out['n'] # spend single key from node 1 rawtx = self.nodes[1].walletcreatefundedpsbt([{"txid": txid, "vout": p2pkh_pos}], { self.nodes[1].getnewaddress(): 9990000})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(rawtx) # Make sure it has UTXOs decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) assert 'utxo' in decoded['inputs'][0] assert_equal(walletprocesspsbt_out['complete'], True) self.nodes[1].sendrawtransaction( self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) inputs = [{"txid": txid, "vout": p2sh_pos}, {"txid": txid, "vout": p2pkh_pos}] output = {self.nodes[1].getnewaddress(): 29_990_000} self.log.info( "Test walletcreatefundedpsbt feeRate of 100,000 XEC/kB produces a " "total fee at or slightly below -maxtxfee") res = self.nodes[1].walletcreatefundedpsbt( inputs, output, 0, {"feeRate": 100_000, "add_inputs": True}) assert_approx(res["fee"], 65000, 5000) self.log.info( "Test walletcreatefundedpsbt feeRate of 10,000,000 XEC/kB produces " "a total fee well above -maxtxfee and raises RPC error") # previously this was silently capped at -maxtxfee for bool_add, output_ in ( (True, output), (False, {self.nodes[1].getnewaddress(): 1_000_000})): assert_raises_rpc_error( -4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, inputs, output_, 0, {"feeRate": 10_000_000, "add_inputs": bool_add}) self.log.info("Test various PSBT operations") # partially sign multisig things with node 1 psbtx = self.nodes[1].walletcreatefundedpsbt([{"txid": txid, "vout": p2sh_pos}], { self.nodes[1].getnewaddress(): 9990000})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx) psbtx = walletprocesspsbt_out['psbt'] assert_equal(walletprocesspsbt_out['complete'], False) # partially sign with node 2. This should be complete and sendable walletprocesspsbt_out = self.nodes[2].walletprocesspsbt(psbtx) assert_equal(walletprocesspsbt_out['complete'], True) self.nodes[2].sendrawtransaction( self.nodes[2].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) # check that walletprocesspsbt fails to decode a non-psbt rawtx = self.nodes[1].createrawtransaction([{"txid": txid, "vout": p2pkh_pos}], { self.nodes[1].getnewaddress(): 9990000}) assert_raises_rpc_error(-22, "TX decode failed", self.nodes[1].walletprocesspsbt, rawtx) # Convert a non-psbt to psbt and make sure we can decode it rawtx = self.nodes[0].createrawtransaction( [], {self.nodes[1].getnewaddress(): 10000000}) rawtx = self.nodes[0].fundrawtransaction(rawtx) new_psbt = self.nodes[0].converttopsbt(rawtx['hex']) self.nodes[0].decodepsbt(new_psbt) # Make sure that a non-psbt with signatures cannot be converted # Error is "Inputs must not have scriptSigs" signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx['hex']) assert_raises_rpc_error(-22, "", self.nodes[0].converttopsbt, signedtx['hex']) assert_raises_rpc_error(-22, "", self.nodes[0].converttopsbt, signedtx['hex'], False) # Unless we allow it to convert and strip signatures self.nodes[0].converttopsbt(signedtx['hex'], True) # Explicilty allow converting non-empty txs new_psbt = self.nodes[0].converttopsbt(rawtx['hex']) self.nodes[0].decodepsbt(new_psbt) # Create outputs to nodes 1 and 2 node1_addr = self.nodes[1].getnewaddress() node2_addr = self.nodes[2].getnewaddress() txid1 = self.nodes[0].sendtoaddress(node1_addr, 13000000) txid2 = self.nodes[0].sendtoaddress(node2_addr, 13000000) blockhash = self.generate(self.nodes[0], 6)[0] - self.sync_all() vout1 = find_output( self.nodes[1], txid1, 13000000, blockhash=blockhash) vout2 = find_output( self.nodes[2], txid2, 13000000, blockhash=blockhash) # Create a psbt spending outputs from nodes 1 and 2 psbt_orig = self.nodes[0].createpsbt([{"txid": txid1, "vout": vout1}, { "txid": txid2, "vout": vout2}], {self.nodes[0].getnewaddress(): 25999000}) # Update psbts, should only have data for one input and not the other psbt1 = self.nodes[1].walletprocesspsbt( psbt_orig, False, "ALL|FORKID")['psbt'] psbt1_decoded = self.nodes[0].decodepsbt(psbt1) assert psbt1_decoded['inputs'][0] and not psbt1_decoded['inputs'][1] # Check that BIP32 path was added assert "bip32_derivs" in psbt1_decoded['inputs'][0] psbt2 = self.nodes[2].walletprocesspsbt( psbt_orig, False, "ALL|FORKID", False)['psbt'] psbt2_decoded = self.nodes[0].decodepsbt(psbt2) assert not psbt2_decoded['inputs'][0] and psbt2_decoded['inputs'][1] # Check that BIP32 paths were not added assert "bip32_derivs" not in psbt2_decoded['inputs'][1] # Sign PSBTs (workaround issue #18039) psbt1 = self.nodes[1].walletprocesspsbt(psbt_orig)['psbt'] psbt2 = self.nodes[2].walletprocesspsbt(psbt_orig)['psbt'] # Combine, finalize, and send the psbts combined = self.nodes[0].combinepsbt([psbt1, psbt2]) finalized = self.nodes[0].finalizepsbt(combined)['hex'] self.nodes[0].sendrawtransaction(finalized) self.generate(self.nodes[0], 6) - self.sync_all() block_height = self.nodes[0].getblockcount() unspent = self.nodes[0].listunspent()[0] # Make sure change address wallet does not have P2SH innerscript access to results in success # when attempting BnB coin selection self.nodes[0].walletcreatefundedpsbt( [], [{self.nodes[2].getnewaddress():unspent["amount"] + 1000000}], block_height + 2, {"changeAddress": self.nodes[1].getnewaddress()}, False) # Regression test for 14473 (mishandling of already-signed # transaction): psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid": unspent["txid"], "vout":unspent["vout"]}], [ {self.nodes[2].getnewaddress():unspent["amount"] + 1000000}], 0, {"add_inputs": True}) complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"]) double_processed_psbt = self.nodes[0].walletprocesspsbt( complete_psbt["psbt"]) assert_equal(complete_psbt, double_processed_psbt) # We don't care about the decode result, but decoding must succeed. self.nodes[0].decodepsbt(double_processed_psbt["psbt"]) # BIP 174 Test Vectors # Check that unknown values are just passed through unknown_psbt = "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" unknown_out = self.nodes[0].walletprocesspsbt(unknown_psbt)['psbt'] assert_equal(unknown_psbt, unknown_out) # Open the data file with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_psbt.json'), encoding='utf-8') as f: d = json.load(f) invalids = d['invalid'] valids = d['valid'] creators = d['creator'] signers = d['signer'] combiners = d['combiner'] finalizers = d['finalizer'] extractors = d['extractor'] # Invalid PSBTs for invalid in invalids: assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decodepsbt, invalid) # Valid PSBTs for valid in valids: self.nodes[0].decodepsbt(valid) # Creator Tests for creator in creators: created_tx = self.nodes[0].createpsbt( creator['inputs'], creator['outputs']) assert_equal(created_tx, creator['result']) # Signer tests for i, signer in enumerate(signers): self.nodes[2].createwallet("wallet{}".format(i)) wrpc = self.nodes[2].get_wallet_rpc("wallet{}".format(i)) for key in signer['privkeys']: wrpc.importprivkey(key) signed_tx = wrpc.walletprocesspsbt(signer['psbt'])['psbt'] assert_equal(signed_tx, signer['result']) # Combiner test for combiner in combiners: combined = self.nodes[2].combinepsbt(combiner['combine']) assert_equal(combined, combiner['result']) # Empty combiner test assert_raises_rpc_error(-8, "Parameter 'txs' cannot be empty", self.nodes[0].combinepsbt, []) # Finalizer test for finalizer in finalizers: finalized = self.nodes[2].finalizepsbt( finalizer['finalize'], False)['psbt'] assert_equal(finalized, finalizer['result']) # Extractor test for extractor in extractors: extracted = self.nodes[2].finalizepsbt( extractor['extract'], True)['hex'] assert_equal(extracted, extractor['result']) # Test decoding error: invalid base64 assert_raises_rpc_error(-22, "TX decode failed invalid base64", self.nodes[0].decodepsbt, ";definitely not base64;") # Test that psbts with p2pkh outputs are created properly p2pkh = self.nodes[0].getnewaddress() psbt = self.nodes[1].walletcreatefundedpsbt( [], [{p2pkh: 1000000}], 0, {"includeWatching": True}, True) self.nodes[0].decodepsbt(psbt['psbt']) # Send to all types of addresses addr1 = self.nodes[1].getnewaddress("") # originally bech32 txid1 = self.nodes[0].sendtoaddress(addr1, 11000000) vout1 = find_output(self.nodes[0], txid1, 11000000) addr2 = self.nodes[1].getnewaddress("") # originally legacy txid2 = self.nodes[0].sendtoaddress(addr2, 11000000) vout2 = find_output(self.nodes[0], txid2, 11000000) addr3 = self.nodes[1].getnewaddress("") # originally p2sh-segwit txid3 = self.nodes[0].sendtoaddress(addr3, 11000000) vout3 = find_output(self.nodes[0], txid3, 11000000) self.sync_all() def test_psbt_input_keys(psbt_input, keys): """Check that the psbt input has only the expected keys.""" assert_equal(set(keys), set(psbt_input.keys())) # Create a PSBT. None of the inputs are filled initially psbt = self.nodes[1].createpsbt([{"txid": txid1, "vout": vout1}, {"txid": txid2, "vout": vout2}, { "txid": txid3, "vout": vout3}], {self.nodes[0].getnewaddress(): 32999000}) decoded = self.nodes[1].decodepsbt(psbt) test_psbt_input_keys(decoded['inputs'][0], []) test_psbt_input_keys(decoded['inputs'][1], []) test_psbt_input_keys(decoded['inputs'][2], []) # Update a PSBT with UTXOs from the node updated = self.nodes[1].utxoupdatepsbt(psbt) decoded = self.nodes[1].decodepsbt(updated) test_psbt_input_keys(decoded['inputs'][1], []) test_psbt_input_keys(decoded['inputs'][2], []) # Try again, now while providing descriptors descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [ addr1, addr2, addr3]] updated = self.nodes[1].utxoupdatepsbt(psbt=psbt, descriptors=descs) decoded = self.nodes[1].decodepsbt(updated) test_psbt_input_keys(decoded['inputs'][1], []) # Two PSBTs with a common input should not be joinable psbt1 = self.nodes[1].createpsbt([{"txid": txid1, "vout": vout1}], { self.nodes[0].getnewaddress(): Decimal('10999000')}) assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated]) # Join two distinct PSBTs addr4 = self.nodes[1].getnewaddress("") txid4 = self.nodes[0].sendtoaddress(addr4, 5000000) vout4 = find_output(self.nodes[0], txid4, 5000000) self.generate(self.nodes[0], 6) - self.sync_all() psbt2 = self.nodes[1].createpsbt([{"txid": txid4, "vout": vout4}], { self.nodes[0].getnewaddress(): Decimal('4999000')}) psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt'] psbt2_decoded = self.nodes[0].decodepsbt(psbt2) assert "final_scriptSig" in psbt2_decoded['inputs'][0] joined = self.nodes[0].joinpsbts([psbt, psbt2]) joined_decoded = self.nodes[0].decodepsbt(joined) assert len(joined_decoded['inputs']) == 4 and len( joined_decoded['outputs']) == 2 and "final_scriptSig" not in joined_decoded['inputs'][3] # Fail when trying to join less than two PSBTs assert_raises_rpc_error(-8, "At least two PSBTs are required to join PSBTs.", self.nodes[1].joinpsbts, []) assert_raises_rpc_error(-8, "At least two PSBTs are required to join PSBTs.", self.nodes[1].joinpsbts, [psbt2]) # Check that joining shuffles the inputs and outputs # 10 attempts should be enough to get a shuffled join shuffled = False for _ in range(10): shuffled_joined = self.nodes[0].joinpsbts([psbt, psbt2]) shuffled |= joined != shuffled_joined if shuffled: break assert shuffled # Newly created PSBT needs UTXOs and updating addr = self.nodes[1].getnewaddress("") txid = self.nodes[0].sendtoaddress(addr, 7000000) blockhash = self.generate(self.nodes[0], 6)[0] - self.sync_all() vout = find_output(self.nodes[0], txid, 7000000, blockhash=blockhash) psbt = self.nodes[1].createpsbt([{"txid": txid, "vout": vout}], { self.nodes[0].getnewaddress(""): Decimal('6999000')}) analyzed = self.nodes[0].analyzepsbt(psbt) assert not analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0][ 'is_final'] and analyzed['inputs'][0]['next'] == 'updater' and analyzed['next'] == 'updater' # After update with wallet, only needs signing updated = self.nodes[1].walletprocesspsbt( psbt, False, 'ALL|FORKID', True)['psbt'] analyzed = self.nodes[0].analyzepsbt(updated) assert analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0][ 'is_final'] and analyzed['inputs'][0]['next'] == 'signer' and analyzed['next'] == 'signer' # Check fee and size things assert analyzed['fee'] == Decimal( '1000') and analyzed['estimated_vsize'] == 191 and analyzed['estimated_feerate'] == Decimal('5235.60') # After signing and finalizing, needs extracting signed = self.nodes[1].walletprocesspsbt(updated)['psbt'] analyzed = self.nodes[0].analyzepsbt(signed) assert analyzed['inputs'][0]['has_utxo'] and analyzed['inputs'][0]['is_final'] and analyzed['next'] == 'extractor' self.log.info( "PSBT spending unspendable outputs should have error message and Creator as next") analysis = self.nodes[0].analyzepsbt( 'cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWAEHYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFv8/wADXYP/7//////8JxOh0LR2HAI8AAAAAAAEAIADC6wsAAAAAF2oUt/X69ELjeX2nTof+fZ10l+OyAokDAQcJAwEHEAABAACAAAEAIADC6wsAAAAAF2oUt/X69ELjeX2nTof+fZ10l+OyAokDAQcJAwEHENkMak8AAAAA') assert_equal(analysis['next'], 'creator') assert_equal( analysis['error'], 'PSBT is not valid. Input 0 spends unspendable output') self.log.info( "PSBT with invalid values should have error message and Creator as next") analysis = self.nodes[0].analyzepsbt( 'cHNidP8BAHECAAAAAfA00BFgAm6tp86RowwH6BMImQNL5zXUcTT97XoLGz0BAAAAAAD/////AgD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XL87QKVAAAAABYAFPck4gF7iL4NL4wtfRAKgQbghiTUAAAAAAABAB8AgIFq49AHABYAFJUDtxf2PHo641HEOBOAIvFMNTr2AAAA') assert_equal(analysis['next'], 'creator') assert_equal( analysis['error'], 'PSBT is not valid. Input 0 has invalid value') analysis = self.nodes[0].analyzepsbt( 'cHNidP8BAHECAAAAAfA00BFgAm6tp86RowwH6BMImQNL5zXUcTT97XoLGz0BAAAAAAD/////AgCAgWrj0AcAFgAUKNw0x8HRctAgmvoevm4u1SbN7XL87QKVAAAAABYAFPck4gF7iL4NL4wtfRAKgQbghiTUAAAAAAABAB8A8gUqAQAAABYAFJUDtxf2PHo641HEOBOAIvFMNTr2AAAA') assert_equal(analysis['next'], 'creator') assert_equal( analysis['error'], 'PSBT is not valid. Output amount invalid') if __name__ == '__main__': PSBTTest().main() diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index d729342ef..1f5955cc9 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -1,722 +1,719 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the rawtranscation RPCs. Test the following RPCs: - createrawtransaction - signrawtransactionwithwallet - sendrawtransaction - decoderawtransaction - getrawtransaction """ from collections import OrderedDict from decimal import Decimal from io import BytesIO from test_framework.messages import ( COutPoint, CTransaction, CTxIn, CTxOut, ToHex, ) from test_framework.script import CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.txtools import pad_raw_tx from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, find_vout_for_address, ) class multidict(dict): """Dictionary that allows duplicate keys. Constructed with a list of (key, value) tuples. When dumped by the json module, will output invalid json with repeated keys, eg: >>> json.dumps(multidict([(1,2),(1,2)]) '{"1": 2, "1": 2}' Used to test calls to rpc methods with repeated keys in the json object.""" def __init__(self, x): dict.__init__(self, x) self.x = x def items(self): return self.x # Create one-input, one-output, no-fee transaction: class RawTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.extra_args = [["-txindex"], ["-txindex"], ["-txindex"]] # whitelist all peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): super().setup_network() self.connect_nodes(0, 2) def run_test(self): self.log.info( 'prepare some coins for multiple *rawtransaction commands') self.generate(self.nodes[2], 1) - self.sync_all() self.generate(self.nodes[0], 101) - self.sync_all() self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1500000) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1000000) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 5000000) self.sync_all() self.generate(self.nodes[0], 5) - self.sync_all() self.log.info( 'Test getrawtransaction on genesis block coinbase returns an error') block = self.nodes[0].getblock(self.nodes[0].getblockhash(0)) assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot']) self.log.info( 'Check parameter types and required parameters of createrawtransaction') # Test `createrawtransaction` required parameters assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction) assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, []) # Test `createrawtransaction` invalid extra parameters assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, 'foo') # Test `createrawtransaction` invalid `inputs` txid = '1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000' assert_raises_rpc_error(-3, "Expected type array", self.nodes[0].createrawtransaction, 'foo', {}) assert_raises_rpc_error(-1, "JSON value is not an object as expected", self.nodes[0].createrawtransaction, ['foo'], {}) assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].createrawtransaction, [{}], {}) assert_raises_rpc_error(-8, "txid must be of length 64 (not 3, for 'foo')", self.nodes[0].createrawtransaction, [{'txid': 'foo'}], {}) 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, vout must be a number", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 'foo'}], {}) 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` address = self.nodes[0].getnewaddress() address2 = self.nodes[0].getnewaddress() assert_raises_rpc_error(-1, "JSON value is not an array as expected", self.nodes[0].createrawtransaction, [], 'foo') # Should not throw for backwards compatibility self.nodes[0].createrawtransaction(inputs=[], outputs={}) self.nodes[0].createrawtransaction(inputs=[], outputs=[]) assert_raises_rpc_error(-8, "Data must be hexadecimal string", self.nodes[0].createrawtransaction, [], {'data': 'foo'}) assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].createrawtransaction, [], {'foo': 0}) assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].createrawtransaction, [], {address: 'foo'}) assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].createrawtransaction, [], {address: -1}) assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: {}".format( address), self.nodes[0].createrawtransaction, [], multidict([(address, 1), (address, 1)])) assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: {}".format( address), self.nodes[0].createrawtransaction, [], [{address: 1}, {address: 1}]) assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data", self.nodes[0].createrawtransaction, [], [{"data": 'aa'}, {"data": "bb"}]) assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data", self.nodes[0].createrawtransaction, [], multidict([("data", 'aa'), ("data", "bb")])) assert_raises_rpc_error(-8, "Invalid parameter, key-value pair must contain exactly one key", self.nodes[0].createrawtransaction, [], [{'a': 1, 'b': 2}]) assert_raises_rpc_error(-8, "Invalid parameter, key-value pair not an object as expected", self.nodes[0].createrawtransaction, [], [['key-value pair1'], ['2']]) # Test `createrawtransaction` invalid `locktime` assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo') assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, -1) assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, 4294967296) self.log.info( 'Check that createrawtransaction accepts an array and object as outputs') tx = CTransaction() # One output tx.deserialize(BytesIO(bytes.fromhex(self.nodes[2].createrawtransaction( inputs=[{'txid': txid, 'vout': 9}], outputs={address: 99})))) assert_equal(len(tx.vout), 1) assert_equal( tx.serialize().hex(), self.nodes[2].createrawtransaction( inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}]), ) # Two outputs tx.deserialize(BytesIO(bytes.fromhex(self.nodes[2].createrawtransaction(inputs=[ {'txid': txid, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)]))))) assert_equal(len(tx.vout), 2) assert_equal( tx.serialize().hex(), self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[ {address: 99}, {address2: 99}]), ) # Multiple mixed outputs tx.deserialize(BytesIO(bytes.fromhex(self.nodes[2].createrawtransaction(inputs=[ {'txid': txid, 'vout': 9}], outputs=multidict([(address, 99), (address2, 99), ('data', '99')]))))) assert_equal(len(tx.vout), 3) assert_equal( tx.serialize().hex(), self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[ {address: 99}, {address2: 99}, {'data': '99'}]), ) for type in ["legacy"]: addr = self.nodes[0].getnewaddress("", type) addrinfo = self.nodes[0].getaddressinfo(addr) pubkey = addrinfo["scriptPubKey"] self.log.info( 'sendrawtransaction with missing prevtx info ({})'.format(type)) # Test `signrawtransactionwithwallet` invalid `prevtxs` inputs = [{'txid': txid, 'vout': 3, 'sequence': 1000}] outputs = {self.nodes[0].getnewaddress(): 1} rawtx = self.nodes[0].createrawtransaction(inputs, outputs) prevtx = dict(txid=txid, scriptPubKey=pubkey, vout=3, amount=1) succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx]) assert succ["complete"] assert_raises_rpc_error(-8, "Missing amount", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { "txid": txid, "scriptPubKey": pubkey, "vout": 3, } ]) assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { "txid": txid, "scriptPubKey": pubkey, "amount": 1, } ]) assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { "scriptPubKey": pubkey, "vout": 3, "amount": 1, } ]) assert_raises_rpc_error(-3, "Missing scriptPubKey", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { "txid": txid, "vout": 3, "amount": 1 } ]) ######################################### # sendrawtransaction with missing input # ######################################### self.log.info('sendrawtransaction with missing input') # won't exists inputs = [ {'txid': "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout': 1}] outputs = {self.nodes[0].getnewaddress(): 4998000} rawtx = self.nodes[2].createrawtransaction(inputs, outputs) rawtx = pad_raw_tx(rawtx) rawtx = self.nodes[2].signrawtransactionwithwallet(rawtx) # This will raise an exception since there are missing inputs assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", self.nodes[2].sendrawtransaction, rawtx['hex']) ##################################### # getrawtransaction with block hash # ##################################### # make a tx by sending then generate 2 blocks; block1 has the tx in it tx = self.nodes[2].sendtoaddress( self.nodes[1].getnewaddress(), 1000000) block1, block2 = self.generate(self.nodes[2], 2) self.sync_all() # We should be able to get the raw transaction by providing the correct # block gottx = self.nodes[0].getrawtransaction(tx, True, block1) assert_equal(gottx['txid'], tx) assert_equal(gottx['in_active_chain'], True) # We should not have the 'in_active_chain' flag when we don't provide a # block gottx = self.nodes[0].getrawtransaction(tx, True) assert_equal(gottx['txid'], tx) assert 'in_active_chain' not in gottx # We should not get the tx if we provide an unrelated block assert_raises_rpc_error(-5, "No such transaction found", self.nodes[0].getrawtransaction, tx, True, block2) # An invalid block hash should raise the correct errors assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].getrawtransaction, tx, True, True) assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 6, for 'foobar')", self.nodes[0].getrawtransaction, tx, True, "foobar") assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 8, for 'abcd1234')", self.nodes[0].getrawtransaction, tx, True, "abcd1234") assert_raises_rpc_error( -8, "parameter 3 must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].getrawtransaction, tx, True, "ZZZ0000000000000000000000000000000000000000000000000000000000000") assert_raises_rpc_error(-5, "Block hash not found", self.nodes[0].getrawtransaction, tx, True, "0000000000000000000000000000000000000000000000000000000000000000") # Undo the blocks and check in_active_chain self.nodes[0].invalidateblock(block1) gottx = self.nodes[0].getrawtransaction( txid=tx, verbose=True, blockhash=block1) assert_equal(gottx['in_active_chain'], False) self.nodes[0].reconsiderblock(block1) assert_equal(self.nodes[0].getbestblockhash(), block2) 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"]) # createmultisig can only take public keys self.nodes[0].createmultisig( 2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # addmultisigaddress can take both pubkeys and addresses so long as # they are in the wallet, which is tested here. assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) mSigObj = self.nodes[2].addmultisigaddress( 2, [addr1Obj['pubkey'], addr1])['address'] # use balance deltas instead of absolute values bal = self.nodes[2].getbalance() # send 1,200,000 XEC to msig adr txId = self.nodes[0].sendtoaddress(mSigObj, 1200000) self.sync_all() self.generate(self.nodes[0], 1) self.sync_all() # node2 has both keys of the 2of2 ms addr., tx should affect the # balance assert_equal(self.nodes[2].getbalance(), bal + Decimal('1200000.00')) # 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, 2200000) decTx = self.nodes[0].gettransaction(txId) rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) self.sync_all() self.generate(self.nodes[0], 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 # for now, assume the funds of a 2of3 multisig tx are not marked as # spendable assert_equal(self.nodes[2].getbalance(), bal) 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('2200000.00')) bal = self.nodes[0].getbalance() inputs = [{ "txid": txId, "vout": vout['n'], "scriptPubKey": vout['scriptPubKey']['hex'], "amount": vout['value'], }] outputs = {self.nodes[0].getnewaddress(): 2190000} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet( rawTx, inputs) # node1 only has one key, can't comp. sign the tx assert_equal(rawTxPartialSigned['complete'], False) rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) # node2 can sign the tx compl., own two of three keys assert_equal(rawTxSigned['complete'], True) self.nodes[2].sendrawtransaction(rawTxSigned['hex']) rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) self.sync_all() self.generate(self.nodes[0], 1) self.sync_all() assert_equal(self.nodes[0].getbalance(), bal + Decimal( '50000000.00') + Decimal('2190000.00')) # block reward + tx rawTxBlock = self.nodes[0].getblock( self.nodes[0].getbestblockhash()) # 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, 2200000) decTx = self.nodes[0].gettransaction(txId) rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) self.sync_all() self.generate(self.nodes[0], 1) self.sync_all() # the funds of a 2of2 multisig tx should not be marked as spendable assert_equal(self.nodes[2].getbalance(), bal) 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('2200000.00')) 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(): 2190000} rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet( rawTx2, inputs) self.log.debug(rawTxPartialSigned1) # node1 only has one key, can't comp. sign the tx assert_equal(rawTxPartialSigned1['complete'], False) rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet( rawTx2, inputs) self.log.debug(rawTxPartialSigned2) # node2 only has one key, can't comp. sign the tx assert_equal(rawTxPartialSigned2['complete'], False) 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.generate(self.nodes[0], 1) self.sync_all() # block reward + tx assert_equal(self.nodes[0].getbalance(), bal + Decimal('50000000.00') + Decimal('2190000.00')) # Sanity checks on verbose getrawtransaction output txId = rawTx["txid"] rawTxOutput = self.nodes[0].getrawtransaction(txId, True) assert_equal(rawTxOutput["hex"], rawTxSigned["hex"]) assert_equal(rawTxOutput["txid"], txId) assert_equal(rawTxOutput["hash"], txId) assert_greater_than(rawTxOutput["size"], 300) assert_equal(rawTxOutput["version"], 0x02) assert_equal(rawTxOutput["locktime"], 0) assert_equal(len(rawTxOutput["vin"]), 1) assert_equal(len(rawTxOutput["vout"]), 1) assert_equal(rawTxOutput["blockhash"], rawTxBlock["hash"]) assert_equal(rawTxOutput["confirmations"], 3) assert_equal(rawTxOutput["time"], rawTxBlock["time"]) assert_equal(rawTxOutput["blocktime"], rawTxBlock["time"]) # Basic signrawtransaction test addr = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(addr, 10_000_000) self.generate(self.nodes[0], 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_000}) rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx) txId = self.nodes[1].sendrawtransaction(rawTxSigned['hex']) self.generate(self.nodes[0], 1) self.sync_all() # getrawtransaction tests # 1. valid parameters - only supply txid assert_equal( self.nodes[0].getrawtransaction(txId), rawTxSigned['hex']) # 2. valid parameters - supply txid and 0 for non-verbose assert_equal( self.nodes[0].getrawtransaction(txId, 0), rawTxSigned['hex']) # 3. valid parameters - supply txid and False for non-verbose assert_equal(self.nodes[0].getrawtransaction(txId, False), rawTxSigned['hex']) # 4. valid parameters - supply txid and 1 for verbose. # We only check the "hex" field of the output so we don't need to # update this test every time the output format changes. assert_equal(self.nodes[0].getrawtransaction(txId, 1)["hex"], rawTxSigned['hex']) # 5. valid parameters - supply txid and True for non-verbose assert_equal(self.nodes[0].getrawtransaction(txId, True)["hex"], rawTxSigned['hex']) # 6. invalid parameters - supply txid and string "Flase" assert_raises_rpc_error(-1, "not a boolean", self.nodes[0].getrawtransaction, txId, "Flase") # 7. invalid parameters - supply txid and empty array assert_raises_rpc_error(-1, "not a boolean", self.nodes[0].getrawtransaction, txId, []) # 8. invalid parameters - supply txid and empty dict assert_raises_rpc_error( -1, "not a boolean", self.nodes[0].getrawtransaction, txId, {}) inputs = [ {'txid': "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'sequence': 1000}] outputs = {self.nodes[0].getnewaddress(): 1} assert_raises_rpc_error( -8, 'Invalid parameter, missing vout key', self.nodes[0].createrawtransaction, inputs, outputs) inputs[0]['vout'] = "1" assert_raises_rpc_error( -8, 'Invalid parameter, vout must be a number', self.nodes[0].createrawtransaction, inputs, outputs) inputs[0]['vout'] = -1 assert_raises_rpc_error( -8, 'Invalid parameter, vout cannot be negative', self.nodes[0].createrawtransaction, inputs, outputs) inputs[0]['vout'] = 1 rawtx = self.nodes[0].createrawtransaction(inputs, outputs) decrawtx = self.nodes[0].decoderawtransaction(rawtx) assert_equal(decrawtx['vin'][0]['sequence'], 1000) # 9. invalid parameters - sequence number out of range inputs[0]['sequence'] = -1 assert_raises_rpc_error( -8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs) # 10. invalid parameters - sequence number out of range inputs[0]['sequence'] = 4294967296 assert_raises_rpc_error( -8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs) inputs[0]['sequence'] = 4294967294 rawtx = self.nodes[0].createrawtransaction(inputs, outputs) decrawtx = self.nodes[0].decoderawtransaction(rawtx) assert_equal(decrawtx['vin'][0]['sequence'], 4294967294) #################################### # TRANSACTION VERSION NUMBER TESTS # #################################### # Test the minimum transaction version number that fits in a signed # 32-bit integer. # As transaction version is unsigned, this should convert to its # unsigned equivalent. tx = CTransaction() tx.nVersion = -0x80000000 rawtx = ToHex(tx) decrawtx = self.nodes[0].decoderawtransaction(rawtx) assert_equal(decrawtx['version'], 0x80000000) # Test the maximum transaction version number that fits in a signed # 32-bit integer. tx = CTransaction() tx.nVersion = 0x7fffffff rawtx = ToHex(tx) decrawtx = self.nodes[0].decoderawtransaction(rawtx) assert_equal(decrawtx['version'], 0x7fffffff) self.log.info('sendrawtransaction/testmempoolaccept with maxfeerate') # Test a transaction with a small fee. txId = self.nodes[0].sendtoaddress( self.nodes[2].getnewaddress(), 1000000) rawTx = self.nodes[0].getrawtransaction(txId, True) vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('1000000.00')) self.sync_all() inputs = [{"txid": txId, "vout": vout['n']}] # Fee 10,000 satoshis, (1,000,000 - (10000 sat * 0.01 XEC/sat)) = # 999900 outputs = {self.nodes[0].getnewaddress(): Decimal("999900.00")} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx) assert_equal(rawTxSigned['complete'], True) # Fee 10,000 satoshis, ~200 b transaction, fee rate should land around 50 sat/byte = 500 XEC/kB # Thus, testmempoolaccept should reject testres = self.nodes[2].testmempoolaccept( [rawTxSigned['hex']], 500.00)[0] assert_equal(testres['allowed'], False) assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw assert_raises_rpc_error(-25, 'Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)', self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 10.00) # and the following calls should both succeed testres = self.nodes[2].testmempoolaccept( rawtxs=[rawTxSigned['hex']])[0] assert_equal(testres['allowed'], True) self.nodes[2].sendrawtransaction(hexstring=rawTxSigned['hex']) # Test a transaction with a large fee. txId = self.nodes[0].sendtoaddress( self.nodes[2].getnewaddress(), 1000000) rawTx = self.nodes[0].getrawtransaction(txId, True) vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('1000000.00')) self.sync_all() inputs = [{"txid": txId, "vout": vout['n']}] # Fee 2,000,000 satoshis, (1,000,000 - (2,000,000 sat * 0.01 XEC/sat)) = # 980000 outputs = {self.nodes[0].getnewaddress(): Decimal("980000.00")} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx) assert_equal(rawTxSigned['complete'], True) # Fee 2,000,000 satoshis, ~100 b transaction, fee rate should land around 20,000 sat/byte = 200,000 XEC/kB # Thus, testmempoolaccept should reject testres = self.nodes[2].testmempoolaccept([rawTxSigned['hex']])[0] assert_equal(testres['allowed'], False) assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw 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='200000.00')[0] assert_equal(testres['allowed'], True) self.nodes[2].sendrawtransaction( hexstring=rawTxSigned['hex'], maxfeerate='200000.00') ########################################## # Decoding weird scripts in transactions # ########################################## self.log.info('Decode correctly-formatted but weird transactions') tx = CTransaction() # empty self.nodes[0].decoderawtransaction(ToHex(tx)) # truncated push tx.vin.append(CTxIn(COutPoint(42, 0), b'\x4e\x00\x00')) tx.vin.append(CTxIn(COutPoint(42, 0), b'\x4c\x10TRUNC')) tx.vout.append(CTxOut(0, b'\x4e\x00\x00')) tx.vout.append(CTxOut(0, b'\x4c\x10TRUNC')) self.nodes[0].decoderawtransaction(ToHex(tx)) # giant pushes and long scripts tx.vin.append( CTxIn(COutPoint(42, 0), CScript([b'giant push' * 10000]))) tx.vout.append(CTxOut(0, CScript([b'giant push' * 10000]))) self.nodes[0].decoderawtransaction(ToHex(tx)) self.log.info('Refuse garbage after transaction') assert_raises_rpc_error(-22, 'TX decode failed', self.nodes[0].decoderawtransaction, ToHex(tx) + '00') if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_txoutproof.py b/test/functional/rpc_txoutproof.py index 43fc9f994..ec8b38809 100755 --- a/test/functional/rpc_txoutproof.py +++ b/test/functional/rpc_txoutproof.py @@ -1,148 +1,146 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2016 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 gettxoutproof and verifytxoutproof RPCs.""" from test_framework.messages import CMerkleBlock, FromHex, ToHex from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet import MiniWallet class MerkleBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ [], ["-txindex"], ] def run_test(self): miniwallet = MiniWallet(self.nodes[0]) # Add enough mature utxos to the wallet, so that all txs spend # confirmed coins self.generate(miniwallet, 5) self.generate(self.nodes[0], 100) - self.sync_all() chain_height = self.nodes[1].getblockcount() assert_equal(chain_height, 105) txid1 = miniwallet.send_self_transfer(from_node=self.nodes[0])['txid'] txid2 = miniwallet.send_self_transfer(from_node=self.nodes[0])['txid'] # This will raise an exception because the transaction is not yet in a # block assert_raises_rpc_error(-5, "Transaction not yet in block", self.nodes[0].gettxoutproof, [txid1]) self.generate(self.nodes[0], 1) blockhash = self.nodes[0].getblockhash(chain_height + 1) txlist = [] blocktxn = self.nodes[0].getblock(blockhash, True)["tx"] txlist.append(blocktxn[1]) txlist.append(blocktxn[2]) assert_equal(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid1])), [txid1]) assert_equal(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid1, txid2])), txlist) assert_equal(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid1, txid2], blockhash)), txlist) # Get the change from txid2 txin_spent = miniwallet.get_utxo() tx3 = miniwallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=txin_spent) txid3 = tx3['txid'] self.generate(self.nodes[0], 1) - self.sync_all() txid_spent = txin_spent["txid"] txid_unspent = txid1 # Input was change from txid2, so txid1 should be unspent # Invalid txids assert_raises_rpc_error( -8, "txid must be of length 64 (not 32, for '00000000000000000000000000000000')", self.nodes[0].gettxoutproof, ["00000000000000000000000000000000"], blockhash) assert_raises_rpc_error( -8, "txid must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].gettxoutproof, ["ZZZ0000000000000000000000000000000000000000000000000000000000000"], blockhash) # Invalid blockhashes assert_raises_rpc_error( -8, "blockhash must be of length 64 (not 32, for '00000000000000000000000000000000')", self.nodes[0].gettxoutproof, [txid_spent], "00000000000000000000000000000000") assert_raises_rpc_error( -8, "blockhash must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].gettxoutproof, [txid_spent], "ZZZ0000000000000000000000000000000000000000000000000000000000000") # We can't find the block from a fully-spent tx assert_raises_rpc_error(-5, "Transaction not yet in block", self.nodes[0].gettxoutproof, [txid_spent]) # We can get the proof if we specify the block assert_equal(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid_spent], blockhash)), [txid_spent]) # We can't get the proof if we specify a non-existent block assert_raises_rpc_error(-5, "Block not found", self.nodes[0].gettxoutproof, [ txid_spent], "0000000000000000000000000000000000000000000000000000000000000000") # We can get the proof if the transaction is unspent assert_equal(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid_unspent])), [txid_unspent]) # We can get the proof if we provide a list of transactions and one of # them is unspent. The ordering of the list should not matter. assert_equal(sorted(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid1, txid2]))), sorted(txlist)) assert_equal(sorted(self.nodes[0].verifytxoutproof( self.nodes[0].gettxoutproof([txid2, txid1]))), sorted(txlist)) # We can always get a proof if we have a -txindex assert_equal(self.nodes[0].verifytxoutproof( self.nodes[1].gettxoutproof([txid_spent])), [txid_spent]) # 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, []) # Test duplicate txid assert_raises_rpc_error(-8, 'Invalid parameter, duplicated txid', self.nodes[0].gettxoutproof, [txid1, txid1]) # Now we'll try tweaking a proof. proof = self.nodes[1].gettxoutproof([txid1, txid2]) assert txid1 in self.nodes[0].verifytxoutproof(proof) assert txid2 in self.nodes[1].verifytxoutproof(proof) tweaked_proof = FromHex(CMerkleBlock(), proof) # Make sure that our serialization/deserialization is working assert txid1 in self.nodes[0].verifytxoutproof(ToHex(tweaked_proof)) # Check to see if we can go up the merkle tree and pass this off as a # single-transaction block tweaked_proof.txn.nTransactions = 1 tweaked_proof.txn.vHash = [tweaked_proof.header.hashMerkleRoot] tweaked_proof.txn.vBits = [True] + [False] * 7 for n in self.nodes: assert not n.verifytxoutproof(ToHex(tweaked_proof)) # TODO: try more variants, eg transactions at different depths, and # verify that the proofs are invalid if __name__ == '__main__': MerkleBlockTest().main() diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index e045f20b9..fc21f605a 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -1,243 +1,242 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the abandontransaction RPC. The abandontransaction RPC marks a transaction and all its in-wallet descendants as abandoned which allows their inputs to be respent. It can be used to replace "stuck" or evicted transactions. It only works on transactions which are not included in a block and are not currently in the mempool. It has no effect on transactions which are already abandoned. """ from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, satoshi_round, ) class AbandonConflictTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-minrelaytxfee=10"], []] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): def total_fees(*txids): total = 0 for txid in txids: # '-=' is because gettransaction(txid)['fee'] returns a negative total -= self.nodes[0].gettransaction(txid)['fee'] return satoshi_round(total) self.generate(self.nodes[1], 100) - self.sync_blocks() balance = self.nodes[0].getbalance() txA = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) txB = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) txC = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) self.sync_mempools() self.generate(self.nodes[1], 1) # Can not abandon non-wallet transaction assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: self.nodes[0].abandontransaction(txid='ff' * 32)) # Can not abandon confirmed transaction assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: self.nodes[0].abandontransaction(txid=txA)) newbalance = self.nodes[0].getbalance() # no more than fees lost assert balance - newbalance <= total_fees(txA, txB, txC) balance = newbalance # Disconnect nodes so node0's transactions don't get into node1's # mempool 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("10000000")) nB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txB)["details"] if tx_out["amount"] == Decimal("10000000")) nC = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txC)["details"] if tx_out["amount"] == Decimal("10000000")) inputs = [] # spend 10btc outputs from txA and txB inputs.append({"txid": txA, "vout": nA}) inputs.append({"txid": txB, "vout": nB}) outputs = {} outputs[self.nodes[0].getnewaddress()] = Decimal("14999980") outputs[self.nodes[1].getnewaddress()] = Decimal("5000000") signed = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) # Identify the 14,999,980 XEC output nAB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txAB1)["details"] if tx_out["amount"] == Decimal("14999980")) # Create a child tx spending AB1 and C inputs = [] # Amount 14,999,980 XEC inputs.append({"txid": txAB1, "vout": nAB}) # Amount 10,000,000 XEC inputs.append({"txid": txC, "vout": nC}) outputs = {} outputs[self.nodes[0].getnewaddress()] = Decimal("24999600") signed2 = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) # Create a child tx spending ABC2 signed3_change = Decimal("24999000") inputs = [{"txid": txABC2, "vout": 0}] outputs = {self.nodes[0].getnewaddress(): signed3_change} signed3 = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) # note tx is never directly referenced, only abandoned as a child of # the above self.nodes[0].sendrawtransaction(signed3["hex"]) # In mempool txs from self should increase balance from change newbalance = self.nodes[0].getbalance() assert_equal( newbalance, balance - Decimal("30000000") + signed3_change) balance = newbalance # Restart the node with a higher min relay fee so the parent tx is no longer in mempool # TODO: redo with eviction self.restart_node(0, extra_args=["-minrelaytxfee=100"]) assert self.nodes[0].getmempoolinfo()['loaded'] # Verify txs no longer in either node's mempool assert_equal(len(self.nodes[0].getrawmempool()), 0) assert_equal(len(self.nodes[1].getrawmempool()), 0) # Transactions which are not in the mempool should only reduce wallet balance. # Transaction inputs should still be spent, but the change not yet # received. newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - signed3_change) # Unconfirmed received funds that are not in mempool also shouldn't show # up in unconfirmed balance. Note that the transactions stored in the wallet # are not necessarily in the node's mempool. balances = self.nodes[0].getbalances()['mine'] assert_equal( balances['untrusted_pending'] + balances['trusted'], newbalance) # Unconfirmed transactions which are not in the mempool should also # not be in listunspent assert txABC2 not in [utxo["txid"] for utxo in self.nodes[0].listunspent(0)] balance = newbalance # Abandon original transaction and verify inputs are available again # including that the child tx was also abandoned self.nodes[0].abandontransaction(txAB1) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance + Decimal("30000000")) balance = newbalance self.log.info("Check abandoned transactions in listsinceblock") listsinceblock = self.nodes[0].listsinceblock() txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send'] for tx in txAB1_listsinceblock: assert_equal(tx['abandoned'], True) assert_equal(tx['confirmations'], 0) assert_equal(tx['trusted'], False) # Verify that even with a low min relay fee, the tx is not re-accepted # from wallet on startup once abandoned. self.restart_node(0, extra_args=["-minrelaytxfee=10"]) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) assert_equal(self.nodes[0].getbalance(), balance) # If the transaction is re-sent the wallet also unabandons it. The # change should be available, and it's child transaction should remain # abandoned. # NOTE: Abandoned transactions are internal to the wallet, and tracked # separately from other indices. self.nodes[0].sendrawtransaction(signed["hex"]) newbalance = self.nodes[0].getbalance() assert_equal( newbalance, balance - Decimal("20000000") + Decimal("14999980")) balance = newbalance # Send child tx again so it is no longer abandoned. self.nodes[0].sendrawtransaction(signed2["hex"]) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("10000000") - Decimal("14999980") + Decimal("24999600")) balance = newbalance # Reset to a higher relay fee so that we abandon a transaction self.restart_node(0, extra_args=["-minrelaytxfee=100"]) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("24999600")) balance = newbalance # Create a double spend of AB1. Spend it again from only A's 10 output. # Mine double spend from node 1. inputs = [] inputs.append({"txid": txA, "vout": nA}) outputs = {} outputs[self.nodes[1].getnewaddress()] = Decimal("9999900") tx = self.nodes[0].createrawtransaction(inputs, outputs) signed = self.nodes[0].signrawtransactionwithwallet(tx) self.nodes[1].sendrawtransaction(signed["hex"]) self.generate(self.nodes[1], 1, sync_fun=self.no_op) self.connect_nodes(0, 1) self.sync_blocks() # Verify that B and C's 10,000,000 XEC outputs are available for # spending again because AB1 is now conflicted newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance + Decimal("20000000")) balance = newbalance # There is currently a minor bug around this and so this test doesn't # work. See Issue #7315 # Invalidate the block with the double spend and B's 10,000,000 XEC # output should no longer be available. Don't think C's should either self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) newbalance = self.nodes[0].getbalance() # assert_equal(newbalance, balance - Decimal("10000000")) self.log.info( "If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer") self.log.info( "conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315") self.log.info(str(balance) + " -> " + str(newbalance) + " ?") if __name__ == '__main__': AbandonConflictTest().main() diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index d892a1dd6..aeef51c4f 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -1,320 +1,318 @@ #!/usr/bin/env python3 # Copyright (c) 2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the wallet can send and receive using all combinations of address types. There are 4 nodes-under-test: - node0 uses legacy addresses - node1 uses legacy addresses - node2 uses legacy addresses - node3 uses legacy addresses node4 exists to generate new blocks. ## Multisig address test Test that adding a multisig address with: - an uncompressed pubkey always gives a legacy address - only compressed pubkeys gives the an `-addresstype` address ## Sending to address types test A series of tests, iterating over node0-node3. In each iteration of the test, one node sends: - 10/101th of its balance to itself (using getrawchangeaddress for single key addresses) - 20/101th to the next node - 30/101th to the node after that - 40/101th to the remaining node - 1/101th remains as fee+change Iterate over each node for single key addresses, and then over each node for multisig addresses. Repeat test. As every node sends coins after receiving, this also verifies that spending coins sent to all these address types works. """ import itertools from decimal import Decimal from test_framework.descriptors import descsum_check, descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_greater_than class AddressTypeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 5 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): self.setup_nodes() # Fully mesh-connect nodes for faster mempool sync for i, j in itertools.product(range(self.num_nodes), repeat=2): if 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)] def test_address(self, node, address, multisig, typ): """Run sanity checks on an address.""" self.log.info(address) info = self.nodes[node].getaddressinfo(address) assert self.nodes[node].validateaddress(address)['isvalid'] assert_equal(info.get('solvable'), True) if not multisig and typ == 'legacy': # P2PKH assert not info['isscript'] assert 'pubkey' in info elif typ == 'legacy': # P2SH-multisig assert info['isscript'] assert_equal(info['script'], 'multisig') assert 'pubkeys' in info else: # Unknown type assert False def test_desc(self, node, address, multisig, typ, utxo): """Run sanity checks on a descriptor reported by getaddressinfo.""" info = self.nodes[node].getaddressinfo(address) assert 'desc' in info assert_equal(info['desc'], utxo['desc']) assert self.nodes[node].validateaddress(address)['isvalid'] # Use a ridiculously roundabout way to find the key origin info through # the PSBT logic. However, this does test consistency between the PSBT reported # fingerprints/paths and the descriptor logic. psbt = self.nodes[node].createpsbt( [{'txid': utxo['txid'], 'vout':utxo['vout']}], [{address: 100.00}]) psbt = self.nodes[node].walletprocesspsbt( psbt, False, "ALL|FORKID", True) decode = self.nodes[node].decodepsbt(psbt['psbt']) key_descs = {} for deriv in decode['inputs'][0]['bip32_derivs']: assert_equal(len(deriv['master_fingerprint']), 8) assert_equal(deriv['path'][0], 'm') key_descs[deriv['pubkey']] = '[' + deriv['master_fingerprint'] + \ deriv['path'][1:] + ']' + deriv['pubkey'] # Verify the descriptor checksum against the Python implementation assert descsum_check(info['desc']) # Verify that stripping the checksum and recreating it using Python # roundtrips assert info['desc'] == descsum_create(info['desc'][:-9]) # Verify that stripping the checksum and feeding it to # getdescriptorinfo roundtrips assert info['desc'] == self.nodes[0].getdescriptorinfo( info['desc'][:-9])['descriptor'] assert_equal( info['desc'][-8:], self.nodes[0].getdescriptorinfo(info['desc'][:-9])['checksum']) # Verify that keeping the checksum and feeding it to getdescriptorinfo # roundtrips assert info['desc'] == self.nodes[0].getdescriptorinfo(info['desc'])[ 'descriptor'] assert_equal(info['desc'][-8:], self.nodes[0].getdescriptorinfo(info['desc'])['checksum']) if not multisig and typ == 'legacy': # P2PKH assert_equal(info['desc'], descsum_create("pkh({})".format(key_descs[info['pubkey']]))) elif typ == 'legacy': # P2SH-multisig assert_equal(info['desc'], descsum_create("sh(multi(2,{},{}))".format( key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))) else: # Unknown type assert False def test_change_output_type( self, node_sender, destinations, expected_type): txid = self.nodes[node_sender].sendmany( dummy="", amounts=dict.fromkeys( destinations, 1000.00)) raw_tx = self.nodes[node_sender].getrawtransaction(txid) tx = self.nodes[node_sender].decoderawtransaction(raw_tx) # Make sure the transaction has change: assert_equal(len(tx["vout"]), len(destinations) + 1) # Make sure the destinations are included, and remove them: output_addresses = [vout['scriptPubKey']['addresses'][0] for vout in tx["vout"]] change_addresses = [ d for d in output_addresses if d not in destinations] assert_equal(len(change_addresses), 1) self.log.debug( "Check if change address " + change_addresses[0] + " is " + expected_type) self.test_address( node_sender, change_addresses[0], multisig=False, typ=expected_type) def run_test(self): # Mine 101 blocks on node4 to bring nodes out of IBD and make sure that # no coinbases are maturing for the nodes-under-test during the test self.generate(self.nodes[4], 101) - self.sync_blocks() uncompressed_1 = "0496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee" uncompressed_2 = "047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77" compressed_1 = "0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52" compressed_2 = "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073" 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') do_multisigs = [False] if not self.options.descriptors: do_multisigs.append(True) for multisig, from_node in itertools.product(do_multisigs, range(4)): self.log.info( "Sending from node {} with{} multisig".format(from_node, "" if multisig else "out")) old_balances = self.get_balances() self.log.debug("Old balances are {}".format(old_balances)) to_send = ( old_balances[from_node] / 101).quantize( Decimal("0.01")) sends = {} addresses = {} self.log.debug("Prepare sends") for n, to_node in enumerate(range(from_node, from_node + 4)): to_node %= 4 if not multisig: if from_node == to_node: # When sending non-multisig to self, use # getrawchangeaddress address = self.nodes[to_node].getrawchangeaddress() else: address = self.nodes[to_node].getnewaddress() else: addr1 = self.nodes[to_node].getnewaddress() addr2 = self.nodes[to_node].getnewaddress() address = self.nodes[to_node].addmultisigaddress(2, [addr1, addr2])[ 'address'] # Do some sanity checking on the created address typ = 'legacy' self.test_address(to_node, address, multisig, typ) # Output entry sends[address] = to_send * 10 * (1 + n) addresses[to_node] = (address, typ) self.log.debug("Sending: {}".format(sends)) self.nodes[from_node].sendmany("", sends) self.sync_mempools() unconf_balances = self.get_balances('untrusted_pending') self.log.debug( "Check unconfirmed balances: {}".format(unconf_balances)) assert_equal(unconf_balances[from_node], 0) for n, to_node in enumerate(range(from_node + 1, from_node + 4)): to_node %= 4 assert_equal(unconf_balances[to_node], to_send * 10 * (2 + n)) # node4 collects fee and block subsidy to keep accounting simple self.generate(self.nodes[4], 1) - self.sync_blocks() # Verify that the receiving wallet contains a UTXO with the # expected address, and expected descriptor for n, to_node in enumerate(range(from_node, from_node + 4)): to_node %= 4 found = False for utxo in self.nodes[to_node].listunspent(): if utxo['address'] == addresses[to_node][0]: found = True self.test_desc( to_node, addresses[to_node][0], multisig, addresses[to_node][1], utxo) break assert found new_balances = self.get_balances() self.log.debug("Check new balances: {}".format(new_balances)) # We don't know what fee was set, so we can only check bounds on # the balance of the sending node assert_greater_than(new_balances[from_node], to_send * 10) assert_greater_than(to_send * 11, new_balances[from_node]) for n, to_node in enumerate(range(from_node + 1, from_node + 4)): to_node %= 4 assert_equal( new_balances[to_node], old_balances[to_node] + to_send * 10 * (2 + n)) # Get addresses from node2 and node3: to_address_2 = self.nodes[2].getnewaddress() to_address_3_1 = self.nodes[3].getnewaddress() to_address_3_2 = self.nodes[3].getnewaddress() self.log.info("Various change output tests") self.test_change_output_type(0, [to_address_3_1], 'legacy') self.test_change_output_type(1, [to_address_2], 'legacy') self.test_change_output_type(1, [to_address_3_1], 'legacy') self.test_change_output_type( 1, [to_address_2, to_address_3_1], 'legacy') self.test_change_output_type( 1, [to_address_3_1, to_address_3_2], 'legacy') self.test_change_output_type(2, [to_address_3_1], 'legacy') self.log.info('Test getrawchangeaddress') self.test_address( 3, self.nodes[3].getrawchangeaddress(), multisig=False, typ='legacy') if __name__ == '__main__': AddressTypeTest().main() diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index a98c9e50b..1f60aefcf 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -1,482 +1,472 @@ #!/usr/bin/env python3 # Copyright (c) 2018 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 the avoid_reuse and setwalletflag features.""" from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, assert_equal, assert_raises_rpc_error, ) def reset_balance(node, discardaddr): '''Throw away all owned coins by the node so it gets a balance of 0.''' balance = node.getbalance(avoid_reuse=False) if balance > 500000: node.sendtoaddress( address=discardaddr, amount=balance, subtractfeefromamount=True, avoid_reuse=False) def count_unspent(node): '''Count the unspent outputs for the given node and return various statistics''' r = { "total": { "count": 0, "sum": 0, }, "reused": { "count": 0, "sum": 0, }, } supports_reused = True for utxo in node.listunspent(minconf=0): r["total"]["count"] += 1 r["total"]["sum"] += utxo["amount"] if supports_reused and "reused" in utxo: if utxo["reused"]: r["reused"]["count"] += 1 r["reused"]["sum"] += utxo["amount"] else: supports_reused = False r["reused"]["supported"] = supports_reused return r def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None): '''Make assertions about a node's unspent output statistics''' stats = count_unspent(node) if total_count is not None: assert_equal(stats["total"]["count"], total_count) if total_sum is not None: assert_approx(stats["total"]["sum"], total_sum, 1000) if reused_supported is not None: assert_equal(stats["reused"]["supported"], reused_supported) if reused_count is not None: assert_equal(stats["reused"]["count"], reused_count) if reused_sum is not None: assert_approx(stats["reused"]["sum"], reused_sum, 0.001) def assert_balances(node, mine): '''Make assertions about a node's getbalances output''' got = node.getbalances()["mine"] for k, v in mine.items(): assert_approx(got[k], v, 1000) class AvoidReuseTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 # This test isn't testing txn relay/timing, so set whitelist on the # peers for instant txn relay. This speeds up the test run time 2-3x. self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): '''Set up initial chain and run tests defined below''' self.test_persistence() self.test_immutable() self.generate(self.nodes[0], 110) - self.sync_all() self.test_change_remains_change(self.nodes[1]) reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_sending_from_reused_address_without_avoid_reuse() reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_sending_from_reused_address_fails() reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_getbalances_used() reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_full_destination_group_is_preferred() reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_all_destination_groups_are_used() def test_persistence(self): '''Test that wallet files persist the avoid_reuse flag.''' self.log.info("Test wallet files persist avoid_reuse flag") # Configure node 1 to use avoid_reuse self.nodes[1].setwalletflag('avoid_reuse') # Flags should be node1.avoid_reuse=false, node2.avoid_reuse=true assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) # Stop and restart node 1 self.restart_node(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) assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) # Attempting to set flag to its current state should throw assert_raises_rpc_error(-8, "Wallet flag is already set to false", self.nodes[0].setwalletflag, 'avoid_reuse', False) assert_raises_rpc_error(-8, "Wallet flag is already set to true", self.nodes[1].setwalletflag, 'avoid_reuse', True) def test_immutable(self): '''Test immutable wallet flags''' self.log.info("Test immutable wallet flags") # Attempt to set the disable_private_keys flag; this should not work assert_raises_rpc_error(-8, "Wallet flag is immutable", self.nodes[1].setwalletflag, 'disable_private_keys') tempwallet = ".wallet_avoidreuse.py_test_immutable_wallet.dat" # Create a wallet with disable_private_keys set; this should work self.nodes[1].createwallet(wallet_name=tempwallet, disable_private_keys=True) w = self.nodes[1].get_wallet_rpc(tempwallet) # Attempt to unset the disable_private_keys flag; this should not work assert_raises_rpc_error(-8, "Wallet flag is immutable", w.setwalletflag, 'disable_private_keys', False) # Unload temp wallet self.nodes[1].unloadwallet(tempwallet) def test_change_remains_change(self, node): self.log.info( "Test that change doesn't turn into non-change when spent") reset_balance(node, node.getnewaddress()) addr = node.getnewaddress() txid = node.sendtoaddress(addr, 1000000) out = node.listunspent( minconf=0, query_options={ 'minimumAmount': 2000000}) assert_equal(len(out), 1) assert_equal(out[0]['txid'], txid) changeaddr = out[0]['address'] # Make sure it's starting out as change as expected assert node.getaddressinfo(changeaddr)['ischange'] for logical_tx in node.listtransactions(): assert logical_tx.get('address') != changeaddr # Spend it reset_balance(node, node.getnewaddress()) # It should still be change assert node.getaddressinfo(changeaddr)['ischange'] for logical_tx in node.listtransactions(): assert logical_tx.get('address') != changeaddr def test_sending_from_reused_address_without_avoid_reuse(self): ''' Test the same as test_sending_from_reused_address_fails, except send the 10MM XEC with the avoid_reuse flag set to false. This means the 10MM XEC send should succeed, where it fails in test_sending_from_reused_address_fails. ''' self.log.info( "Test sending from reused address with avoid_reuse=false") fundaddr = self.nodes[1].getnewaddress() retaddr = self.nodes[0].getnewaddress() self.nodes[0].sendtoaddress(fundaddr, 10000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 1 single, unused 10MM XEC output assert_unspent( self.nodes[1], total_count=1, total_sum=10000000, reused_supported=True, reused_count=0) # getbalances should show no used, 10MM XEC trusted assert_balances(self.nodes[1], mine={"used": 0, "trusted": 10000000}) # node 0 should not show a used entry, as it does not enable # avoid_reuse assert "used" not in self.nodes[0].getbalances()["mine"] self.nodes[1].sendtoaddress(retaddr, 5000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 1 single, unused 5MM XEC output assert_unspent( self.nodes[1], total_count=1, total_sum=5000000, reused_supported=True, reused_count=0) # getbalances should show no used, 5MM XEC trusted assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5000000}) self.nodes[0].sendtoaddress(fundaddr, 10000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 2 total outputs (5MM, 10MM XEC), one unused # (5MM), one reused (10MM) assert_unspent( self.nodes[1], total_count=2, total_sum=15000000, reused_count=1, reused_sum=10000000) # getbalances should show 10MM used, 5MM XEC trusted assert_balances( self.nodes[1], mine={ "used": 10000000, "trusted": 5000000}) self.nodes[1].sendtoaddress( address=retaddr, amount=10000000, avoid_reuse=False) # listunspent should show 1 total outputs (5MM XEC), unused assert_unspent( self.nodes[1], total_count=1, total_sum=5000000, reused_count=0) # getbalances should show no used, 5MM XEC trusted assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5000000}) # node 1 should now have about 5MM XEC left (for both cases) assert_approx(self.nodes[1].getbalance(), 5000000, 1000) assert_approx( self.nodes[1].getbalance( avoid_reuse=False), 5000000, 1000) def test_sending_from_reused_address_fails(self): ''' Test the simple case where [1] generates a new address A, then [0] sends 10MM XEC to A. [1] spends 5MM XEC from A. (leaving roughly 5MM XEC useable) [0] sends 10MM XEC to A again. [1] tries to spend 10MM XEC (fails; dirty). [1] tries to spend 4MM XEC (succeeds; change address sufficient) ''' self.log.info("Test sending from reused address fails") fundaddr = self.nodes[1].getnewaddress(label="", address_type="legacy") retaddr = self.nodes[0].getnewaddress() self.nodes[0].sendtoaddress(fundaddr, 10000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 1 single, unused 10MM XEC output assert_unspent( self.nodes[1], total_count=1, total_sum=10000000, reused_supported=True, reused_count=0) # getbalances should show no used, 10MM XEC trusted assert_balances(self.nodes[1], mine={"used": 0, "trusted": 10000000}) self.nodes[1].sendtoaddress(retaddr, 5000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 1 single, unused 5MM XEC output assert_unspent( self.nodes[1], total_count=1, total_sum=5000000, reused_supported=True, reused_count=0) # getbalances should show no used, 5MM XEC trusted assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5000000}) if not self.options.descriptors: # For the second send, we transmute it to a related single-key address # to make sure it's also detected as re-use # NB: this is not very useful for ABC, but we keep the new variable # name for consistency. new_fundaddr = fundaddr self.nodes[0].sendtoaddress(new_fundaddr, 10000000) self.generate(self.nodes[0], 1) - self.sync_all() # listunspent should show 2 total outputs (5MM, 10MM XEC), one unused # (5MM), one reused (10MM) assert_unspent( self.nodes[1], total_count=2, total_sum=15000000, reused_count=1, reused_sum=10000000) # getbalances should show 10MM used, 5MM XEC trusted assert_balances( self.nodes[1], mine={ "used": 10000000, "trusted": 5000000}) # node 1 should now have a balance of 5MM (no dirty) or 15MM # (including dirty) assert_approx(self.nodes[1].getbalance(), 5000000, 1000) assert_approx( self.nodes[1].getbalance( avoid_reuse=False), 15000000, 1000) assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10000000) self.nodes[1].sendtoaddress(retaddr, 4000000) # listunspent should show 2 total outputs (1MM, 10MM XEC), one unused # (1MM), one reused (10MM) assert_unspent( self.nodes[1], total_count=2, total_sum=11000000, reused_count=1, reused_sum=10000000) # getbalances should show 10MM used, 1MM XEC trusted assert_balances( self.nodes[1], mine={ "used": 10000000, "trusted": 1000000}) # node 1 should now have about 1MM XEC left (no dirty) and 11MM # (including dirty) assert_approx(self.nodes[1].getbalance(), 1000000, 1000) assert_approx( self.nodes[1].getbalance( avoid_reuse=False), 11000000, 1000) def test_getbalances_used(self): ''' getbalances and listunspent should pick up on reused addresses immediately, even for address reusing outputs created before the first transaction was spending from that address ''' self.log.info("Test getbalances used category") # node under test should be completely empty assert_equal(self.nodes[1].getbalance(avoid_reuse=False), 0) new_addr = self.nodes[1].getnewaddress() ret_addr = self.nodes[0].getnewaddress() # send multiple transactions, reusing one address for _ in range(11): self.nodes[0].sendtoaddress(new_addr, 1000000) self.generate(self.nodes[0], 1) - self.sync_all() # send transaction that should not use all the available outputs # per the current coin selection algorithm self.nodes[1].sendtoaddress(ret_addr, 5000000) # getbalances and listunspent should show the remaining outputs # in the reused address as used/reused assert_unspent( self.nodes[1], total_count=2, total_sum=6000000, reused_count=1, reused_sum=1000000) assert_balances( self.nodes[1], mine={ "used": 1000000, "trusted": 5000000}) def test_full_destination_group_is_preferred(self): ''' Test the case where [1] only has 11 outputs of 1MM XEC in the same reused address and tries to send a small payment of 500,000 XEC. The wallet should use 10 outputs from the reused address as inputs and not a single 1MM XEC input, in order to join several outputs from the reused address. ''' self.log.info( "Test that full destination groups are preferred in coin selection") # Node under test should be empty assert_equal(self.nodes[1].getbalance(avoid_reuse=False), 0) new_addr = self.nodes[1].getnewaddress() ret_addr = self.nodes[0].getnewaddress() # Send 11 outputs of 1MM XEC to the same, reused address in the wallet for _ in range(11): self.nodes[0].sendtoaddress(new_addr, 1000000) self.generate(self.nodes[0], 1) - self.sync_all() # Sending a transaction that is smaller than each one of the # available outputs txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=500000) inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"] # The transaction should use 10 inputs exactly assert_equal(len(inputs), 10) def test_all_destination_groups_are_used(self): ''' Test the case where [1] only has 22 outputs of 1MM XEC in the same reused address and tries to send a payment of 20,5MM XEC. The wallet should use all 22 outputs from the reused address as inputs. ''' self.log.info("Test that all destination groups are used") # Node under test should be empty assert_equal(self.nodes[1].getbalance(avoid_reuse=False), 0) new_addr = self.nodes[1].getnewaddress() ret_addr = self.nodes[0].getnewaddress() # Send 22 outputs of 1MM XEC to the same, reused address in the wallet for _ in range(22): self.nodes[0].sendtoaddress(new_addr, 1000000) self.generate(self.nodes[0], 1) - self.sync_all() # Sending a transaction that needs to use the full groups # of 10 inputs but also the incomplete group of 2 inputs. txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=20500000) inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"] # The transaction should use 22 inputs exactly assert_equal(len(inputs), 22) if __name__ == '__main__': AvoidReuseTest().main() diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index b6c56d506..6cabfbc31 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -1,255 +1,249 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet backup features. Test case is: 4 nodes. 1 2 and 3 send transactions between each other, fourth node is a miner. 1 2 3 each mine a block to start, then Miner creates 100 blocks so 1 2 3 each have 50 mature coins to spend. Then 5 iterations of 1/2/3 sending coins amongst themselves to get transactions in the wallets, and the miner mining one block. Wallets are backed up using dumpwallet/backupwallet. Then 5 more iterations of transactions and mining a block. Miner then generates 101 more blocks, so any transaction fees paid mature. Sanity check: Sum(1,2,3,4 balances) == 114*50 1/2/3 are shutdown, and their wallets erased. Then restore using wallet.dat backup. And confirm 1/2/3/4 balances are same as before. Shutdown again, restore using importwallet, and confirm again balances are correct. """ import os import shutil from decimal import Decimal from random import randint from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error class WalletBackupTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True # 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"], ["-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 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): self.setup_nodes() 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): if (randint(1, 2) == 1): amount = Decimal(randint(1, 10)) * Decimal(100000) self.nodes[from_node].sendtoaddress(to_address, amount) def do_one_round(self): a0 = self.nodes[0].getnewaddress() a1 = self.nodes[1].getnewaddress() a2 = self.nodes[2].getnewaddress() self.one_send(0, a1) self.one_send(0, a2) self.one_send(1, a0) self.one_send(1, a2) self.one_send(2, a0) self.one_send(2, a1) # Have the miner (node3) mine a block. # Must sync mempools before mining. self.sync_mempools() self.generate(self.nodes[3], 1) - 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) 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) self.stop_node(1) self.stop_node(2) def erase_three(self): 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 run_test(self): self.log.info("Generating initial blockchain") self.generate(self.nodes[0], 1) - self.sync_blocks() self.generate(self.nodes[1], 1) - self.sync_blocks() self.generate(self.nodes[2], 1) - self.sync_blocks() self.generate(self.nodes[3], 100) - self.sync_blocks() assert_equal(self.nodes[0].getbalance(), 50000000) assert_equal(self.nodes[1].getbalance(), 50000000) assert_equal(self.nodes[2].getbalance(), 50000000) assert_equal(self.nodes[3].getbalance(), 0) self.log.info("Creating transactions") # Five rounds of sending each other transactions. for _ in range(5): self.do_one_round() 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')) self.log.info("More transactions") for _ in range(5): self.do_one_round() # Generate 101 more blocks, so any fees paid mature self.generate(self.nodes[3], 101) - self.sync_all() balance0 = self.nodes[0].getbalance() balance1 = self.nodes[1].getbalance() balance2 = self.nodes[2].getbalance() balance3 = self.nodes[3].getbalance() total = balance0 + balance1 + balance2 + balance3 # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.) # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700. assert_equal(total, 5700000000) ## # Test restoring spender wallets from backups ## self.log.info("Restoring using wallet.dat") 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')) # 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', 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() self.sync_blocks() assert_equal(self.nodes[0].getbalance(), balance0) 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() # 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() 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.sync_blocks() 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', 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: assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath) if __name__ == '__main__': WalletBackupTest().main() diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index cdfaafcc1..010039b4c 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -1,345 +1,339 @@ #!/usr/bin/env python3 # Copyright (c) 2018-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet balance RPC methods.""" import struct from decimal import Decimal from test_framework.address import ( ADDRESS_ECREG_UNSPENDABLE as ADDRESS_WATCHONLY, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error def create_transactions(node, address, amt, fees): # Create and sign raw transactions from node to address for amt. # Creates a transaction for each fee and returns an array # of the raw transactions. utxos = [u for u in node.listunspent(0) if u['spendable']] # Create transactions inputs = [] ins_total = 0 for utxo in utxos: inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]}) ins_total += utxo['amount'] if ins_total >= amt + max(fees): break # make sure there was enough utxos assert ins_total >= amt + max(fees) txs = [] for fee in fees: outputs = {address: amt} # prevent 0 change output if ins_total > amt + fee: outputs[node.getrawchangeaddress()] = ins_total - amt - fee raw_tx = node.createrawtransaction(inputs, outputs, 0) raw_tx = node.signrawtransactionwithwallet(raw_tx) assert_equal(raw_tx['complete'], True) txs.append(raw_tx) return txs class WalletTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ # Limit mempool descendants as a hack to have wallet txs rejected # from the mempool ['-limitdescendantcount=3'], [], ] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): 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("Mining blocks ...") self.generate(self.nodes[0], 1) - self.sync_all() self.generate(self.nodes[1], 1) self.generatetoaddress(self.nodes[1], 101, ADDRESS_WATCHONLY) - self.sync_all() 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'], 50000000) assert_equal(self.nodes[0].getwalletinfo()['balance'], 50000000) assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50000000) assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000000000) assert 'watchonly' not in self.nodes[1].getbalances() assert_equal(self.nodes[0].getbalance(), 50000000) assert_equal(self.nodes[1].getbalance(), 50000000) self.log.info("Test getbalance with different arguments") assert_equal(self.nodes[0].getbalance("*"), 50000000) assert_equal(self.nodes[0].getbalance("*", 1), 50000000) assert_equal(self.nodes[0].getbalance(minconf=1), 50000000) if not self.options.descriptors: assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 100_000_000) assert_equal(self.nodes[0].getbalance("*", 1, True), 100_000_000) else: assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 50_000_000) assert_equal(self.nodes[0].getbalance("*", 1, True), 50_000_000) assert_equal( self.nodes[1].getbalance( minconf=0, include_watchonly=True), 50000000) # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. txs = create_transactions( self.nodes[0], self.nodes[1].getnewaddress(), 40000000, [Decimal('10000')]) self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[1].sendrawtransaction(txs[0]['hex']) self.sync_all() txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60000000, [ Decimal('10000'), Decimal('20000')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[0].sendrawtransaction(txs[0]['hex']) self.sync_all() # First argument of getbalance must be set to "*" assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "") self.log.info("Test balances with unconfirmed inputs") # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to #16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After #16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after #16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post #16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. def test_balances(*, fee_node_1=0): # getbalances expected_balances_0 = {'mine': {'immature': Decimal('0E-2'), # change from node 0's send 'trusted': Decimal('9990000'), 'untrusted_pending': Decimal('60000000.0')}, 'watchonly': {'immature': Decimal('5000000000'), 'trusted': Decimal('50000000.0'), 'untrusted_pending': Decimal('0E-2')}} expected_balances_1 = {'mine': {'immature': Decimal('0E-2'), # node 1's send had an unsafe input 'trusted': Decimal('0E-2'), # Doesn't include output of node # 0's send since it was spent 'untrusted_pending': Decimal('30000000.0') - fee_node_1}} 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 # change from node 0's send assert_equal(self.nodes[0].getbalance(), Decimal('9990000')) # node 1's send had an unsafe input assert_equal(self.nodes[1].getbalance(), Decimal('0')) # Same with minconf=0 assert_equal( self.nodes[0].getbalance( minconf=0), Decimal('9990000')) assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('0')) # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth assert_equal(self.nodes[0].getbalance(minconf=1), Decimal('0')) assert_equal(self.nodes[1].getbalance(minconf=1), Decimal('0')) # getunconfirmedbalance # output of node 1's spend assert_equal( self.nodes[0].getunconfirmedbalance(), Decimal('60000000')) # Doesn't include output of node 0's send since it was spent assert_equal( self.nodes[1].getunconfirmedbalance(), Decimal('30000000') - fee_node_1) # getwalletinfo.unconfirmed_balance assert_equal(self.nodes[0].getwalletinfo()[ "unconfirmed_balance"], Decimal('60000000')) assert_equal( self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('30000000') - fee_node_1) test_balances(fee_node_1=Decimal('10000')) # In the original Core version of this test, Node 1 would've bumped # the fee by 0.01 here to resend, but this is XEC, so it has 10000 XEC # left to spend on goods and services self.sync_all() self.log.info( "Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs") test_balances(fee_node_1=Decimal('10000')) self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY) - self.sync_all() # balances are correct after the transactions are confirmed # node 1's send plus change from node 0's send balance_node0 = Decimal('69990000') # change from node 0's send balance_node1 = Decimal('29990000') assert_equal(self.nodes[0].getbalances()[ 'mine']['trusted'], balance_node0) assert_equal(self.nodes[1].getbalances()[ 'mine']['trusted'], balance_node1) assert_equal(self.nodes[0].getbalance(), balance_node0) assert_equal(self.nodes[1].getbalance(), balance_node1) # Send total balance away from node 1 txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress( ), Decimal('29970000'), [Decimal('10000')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) self.generatetoaddress(self.nodes[1], 2, ADDRESS_WATCHONLY) - self.sync_all() # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth # getbalance with minconf=3 should still show the old balance assert_equal(self.nodes[1].getbalance(minconf=3), Decimal('0')) # getbalance with minconf=2 will show the new balance. assert_equal(self.nodes[1].getbalance(minconf=2), Decimal('10000')) # check mempool transactions count for wallet unconfirmed balance after # dynamically loading the wallet. before = self.nodes[1].getbalances()['mine']['untrusted_pending'] dst = self.nodes[1].getnewaddress() self.nodes[1].unloadwallet(self.default_wallet_name) self.nodes[0].sendtoaddress(dst, 100000) self.sync_all() self.nodes[1].loadwallet(self.default_wallet_name) after = self.nodes[1].getbalances()['mine']['untrusted_pending'] assert_equal(before + Decimal('100000'), after) # Create 3 more wallet txs, where the last is not accepted to the # mempool because it is the third descendant of the tx above for _ in range(3): # Set amount high enough such that all coins are spent by each tx txid = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), 99000000) self.log.info('Check that wallet txs not in the mempool are untrusted') assert txid not in self.nodes[0].getrawmempool() assert_equal(self.nodes[0].gettransaction(txid)['trusted'], False) assert_equal(self.nodes[0].getbalance(minconf=0), 0) self.log.info("Test replacement and reorg of non-mempool tx") tx_orig = self.nodes[0].gettransaction(txid)['hex'] # Increase fee by 1 coin tx_replace = tx_orig.replace( struct.pack(" 1 for address in output_addresses: ischange = self.nodes[0].getaddressinfo(address)['ischange'] assert_equal(ischange, address != destination) if ischange: change = address self.nodes[0].setlabel(change, 'foobar') assert_equal(self.nodes[0].getaddressinfo(change)['ischange'], False) # Test gettransaction response with different arguments. self.log.info( "Testing gettransaction response with different arguments...") self.nodes[0].setlabel(change, 'baz') baz = self.nodes[0].listtransactions(label="baz", count=1)[0] expected_receive_vout = {"label": "baz", "address": baz["address"], "amount": baz["amount"], "category": baz["category"], "vout": baz["vout"]} expected_fields = frozenset({'amount', 'confirmations', 'details', 'fee', 'hex', 'time', 'timereceived', 'trusted', 'txid', 'walletconflicts'}) verbose_field = "decoded" expected_verbose_fields = expected_fields | {verbose_field} self.log.debug("Testing gettransaction response without verbose") tx = self.nodes[0].gettransaction(txid=txid) assert_equal(set([*tx]), expected_fields) assert_array_result( tx["details"], { "category": "receive"}, expected_receive_vout) self.log.debug( "Testing gettransaction response with verbose set to False") tx = self.nodes[0].gettransaction(txid=txid, verbose=False) assert_equal(set([*tx]), expected_fields) assert_array_result( tx["details"], { "category": "receive"}, expected_receive_vout) self.log.debug( "Testing gettransaction response with verbose set to True") tx = self.nodes[0].gettransaction(txid=txid, verbose=True) assert_equal(set([*tx]), expected_verbose_fields) assert_array_result( tx["details"], { "category": "receive"}, expected_receive_vout) assert_equal( tx[verbose_field], self.nodes[0].decoderawtransaction( tx["hex"])) if __name__ == '__main__': WalletTest().main() diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py index 7ab405458..bd27df92f 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -1,186 +1,180 @@ #!/usr/bin/env python3 # Copyright (c) 2018 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 wallet group functionality.""" from test_framework.messages import CTransaction, FromHex, ToHex from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_approx, assert_equal class WalletGroupTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 5 self.extra_args = [ [], [], ['-avoidpartialspends'], # 2.93 XEC is the threshold that causes a node to prefer a # non-grouped tx (3 inputs, 2 outputs) to a grouped tx (5 inputs, # 2 outputs). The fee for the grouped tx is 294 sats higher than # the fee for the non-grouped tx. See tx5 below. ["-maxapsfee=2.93"], ["-maxapsfee=2.94"]] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") self.rpc_timeout = 480 self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Mine some coins self.generate(self.nodes[0], 110) # Get some addresses from the two nodes addr1 = [self.nodes[1].getnewaddress() for _ in range(3)] addr2 = [self.nodes[2].getnewaddress() for _ in range(3)] addrs = addr1 + addr2 # Send 1 + 0.5 coin to each address [self.nodes[0].sendtoaddress(addr, 1000000) for addr in addrs] [self.nodes[0].sendtoaddress(addr, 500000) for addr in addrs] self.generate(self.nodes[0], 1) - self.sync_all() # For each node, send 0.2 coins back to 0; # - node[1] should pick one 0.5 UTXO and leave the rest # - node[2] should pick one (1.0 + 0.5) UTXO group corresponding to a # given address, and leave the rest txid1 = self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), 200000) tx1 = self.nodes[1].getrawtransaction(txid1, True) # txid1 should have 1 input and 2 outputs assert_equal(1, len(tx1["vin"])) assert_equal(2, len(tx1["vout"])) # one output should be 0.2, the other should be ~0.3 v = sorted([vout["value"] for vout in tx1["vout"]]) assert_approx(v[0], vexp=200_000, vspan=100) assert_approx(v[1], vexp=300_000, vspan=100) txid2 = self.nodes[2].sendtoaddress( self.nodes[0].getnewaddress(), 200000) tx2 = self.nodes[2].getrawtransaction(txid2, True) # txid2 should have 2 inputs and 2 outputs assert_equal(2, len(tx2["vin"])) assert_equal(2, len(tx2["vout"])) # one output should be 0.2, the other should be ~1.3 v = sorted([vout["value"] for vout in tx2["vout"]]) assert_approx(v[0], vexp=200_000, vspan=100) assert_approx(v[1], vexp=1_300_000, vspan=100) # Test 'avoid partial if warranted, even if disabled' self.sync_all() self.generate(self.nodes[0], 1) # Nodes 1-2 now have confirmed UTXOs (letters denote destinations): # Node #1: Node #2: # - A 1.0 - D0 1.0 # - B0 1.0 - D1 0.5 # - B1 0.5 - E0 1.0 # - C0 1.0 - E1 0.5 # - C1 0.5 - F ~1.3 # - D ~0.3 assert_approx(self.nodes[1].getbalance(), vexp=4_300_000, vspan=100) assert_approx(self.nodes[2].getbalance(), vexp=4_300_000, vspan=100) # Sending 1.4 btc should pick one 1.0 + one more. For node #1, # this could be (A / B0 / C0) + (B1 / C1 / D). We ensure that it is # B0 + B1 or C0 + C1, because this avoids partial spends while not being # detrimental to transaction cost txid3 = self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), 1400000) tx3 = self.nodes[1].getrawtransaction(txid3, True) # tx3 should have 2 inputs and 2 outputs assert_equal(2, len(tx3["vin"])) assert_equal(2, len(tx3["vout"])) # the accumulated value should be 1.5, so the outputs should be # ~0.1 and 1.4 and should come from the same destination values = sorted([vout["value"] for vout in tx3["vout"]]) assert_approx(values[0], vexp=100_000, vspan=100) assert_approx(values[1], vexp=1_400_000, vspan=100) input_txids = [vin["txid"] for vin in tx3["vin"]] input_addrs = [self.nodes[1].gettransaction( txid)['details'][0]['address'] for txid in input_txids] assert_equal(input_addrs[0], input_addrs[1]) # Node 2 enforces avoidpartialspends so needs no checking here # Test wallet option maxapsfee with Node 3 addr_aps = self.nodes[3].getnewaddress() self.nodes[0].sendtoaddress(addr_aps, 1000000) self.nodes[0].sendtoaddress(addr_aps, 1000000) self.generate(self.nodes[0], 1) - self.sync_all() with self.nodes[3].assert_debug_log([ 'Fee non-grouped = 225, grouped = 372, using grouped']): txid4 = self.nodes[3].sendtoaddress( self.nodes[0].getnewaddress(), 100_000) tx4 = self.nodes[3].getrawtransaction(txid4, True) # tx4 should have 2 inputs and 2 outputs although one output would # have been enough and the transaction caused higher fees assert_equal(2, len(tx4["vin"])) assert_equal(2, len(tx4["vout"])) addr_aps2 = self.nodes[3].getnewaddress() [self.nodes[0].sendtoaddress(addr_aps2, 1_000_000) for _ in range(5)] self.generate(self.nodes[0], 1) - self.sync_all() with self.nodes[3].assert_debug_log([ 'Fee non-grouped = 519, grouped = 813, using non-grouped']): txid5 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 2_950_000) tx5 = self.nodes[3].getrawtransaction(txid5, True) # tx5 should have 3 inputs (1.0, 1.0, 1.0) and 2 outputs assert_equal(3, len(tx5["vin"])) assert_equal(2, len(tx5["vout"])) # Test wallet option maxapsfee with node 4, which sets maxapsfee # 1 sat higher, crossing the threshold from non-grouped to grouped. addr_aps3 = self.nodes[4].getnewaddress() [self.nodes[0].sendtoaddress(addr_aps3, 1_000_000) for _ in range(5)] self.generate(self.nodes[0], 1) - self.sync_all() with self.nodes[4].assert_debug_log([ 'Fee non-grouped = 519, grouped = 813, using grouped']): txid6 = self.nodes[4].sendtoaddress(self.nodes[0].getnewaddress(), 2_950_000) tx6 = self.nodes[4].getrawtransaction(txid6, True) # tx6 should have 5 inputs and 2 outputs assert_equal(5, len(tx6["vin"])) assert_equal(2, len(tx6["vout"])) # Empty out node2's wallet self.nodes[2].sendtoaddress(address=self.nodes[0].getnewaddress( ), amount=self.nodes[2].getbalance(), subtractfeefromamount=True) self.sync_all() self.generate(self.nodes[0], 1) # Fill node2's wallet with 10000 outputs corresponding to the same # scriptPubKey for _ in range(5): raw_tx = self.nodes[0].createrawtransaction( [{"txid": "0" * 64, "vout": 0}], [{addr2[0]: 50_000}]) tx = FromHex(CTransaction(), raw_tx) tx.vin = [] tx.vout = [tx.vout[0]] * 2000 funded_tx = self.nodes[0].fundrawtransaction(ToHex(tx)) signed_tx = self.nodes[0].signrawtransactionwithwallet( funded_tx['hex']) self.nodes[0].sendrawtransaction(signed_tx['hex']) self.generate(self.nodes[0], 1) - self.sync_all() - # Check that we can create a transaction that only requires ~100 of our # utxos, without pulling in all outputs and creating a transaction that # is way too big. assert self.nodes[2].sendtoaddress(address=addr2[0], amount=5000000) if __name__ == '__main__': WalletGroupTest().main() diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index f8c1532df..25049d34f 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -1,355 +1,353 @@ #!/usr/bin/env python3 # Copyright (c) 2016-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test Hierarchical Deterministic wallet function.""" import os import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error class WalletHDTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [[], ['-keypool=0']] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Make sure we use hd, keep masterkeyid hd_fingerprint = self.nodes[1].getaddressinfo( self.nodes[1].getnewaddress())['hdmasterfingerprint'] assert_equal(len(hd_fingerprint), 8) # create an internal key change_addr = self.nodes[1].getrawchangeaddress() change_addrV = self.nodes[1].getaddressinfo(change_addr) if self.options.descriptors: assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/0") else: # first internal child key assert_equal(change_addrV["hdkeypath"], "m/0'/1'/0'") # Import a non-HD private key in the HD wallet non_hd_add = 'ecregtest:qr09jgufyeae4s97nqp6mv0tv6eymfunygeyv0llfe' non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt' self.nodes[1].importprivkey(non_hd_key) # This should be enough to keep the master key and the non-HD key self.nodes[1].backupwallet( os.path.join(self.nodes[1].datadir, "hd.bak")) # self.nodes[1].dumpwallet(os.path.join(self.nodes[1].datadir, "hd.dump")) # Derive some HD addresses and remember the last # Also send funds to each add self.generate(self.nodes[0], 101) hd_add = None NUM_HD_ADDS = 10 for i in range(1, NUM_HD_ADDS + 1): hd_add = self.nodes[1].getnewaddress() hd_info = self.nodes[1].getaddressinfo(hd_add) if self.options.descriptors: assert_equal(hd_info["hdkeypath"], "m/44'/1'/0'/0/" + str(i)) else: assert_equal(hd_info["hdkeypath"], "m/0'/0'/" + str(i) + "'") assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint) self.nodes[0].sendtoaddress(hd_add, 1000000) self.generate(self.nodes[0], 1) self.nodes[0].sendtoaddress(non_hd_add, 1000000) self.generate(self.nodes[0], 1) # create an internal key (again) change_addr = self.nodes[1].getrawchangeaddress() change_addrV = self.nodes[1].getaddressinfo(change_addr) if self.options.descriptors: assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/1") else: # second internal child key assert_equal(change_addrV["hdkeypath"], "m/0'/1'/1'") self.sync_all() assert_equal( self.nodes[1].getbalance(), (NUM_HD_ADDS * 1000000) + 1000000) self.log.info("Restore backup ...") self.stop_node(1) # we need to delete the complete regtest directory # otherwise node1 would auto-recover all funds in flag the keypool keys # as used shutil.rmtree( os.path.join( self.nodes[1].datadir, self.chain, "blocks")) 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', self.default_wallet_name, self.wallet_data_filename)) self.start_node(1) # Assert that derivation is deterministic hd_add_2 = None for i in range(1, NUM_HD_ADDS + 1): hd_add_2 = self.nodes[1].getnewaddress() hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2) if self.options.descriptors: assert_equal(hd_info_2["hdkeypath"], "m/44'/1'/0'/0/" + str(i)) else: 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) self.connect_nodes(0, 1) self.sync_all() # Needs rescan self.restart_node(1, extra_args=self.extra_args[1] + ['-rescan']) assert_equal( self.nodes[1].getbalance(), (NUM_HD_ADDS * 1000000) + 1000000) # Try a RPC based rescan self.stop_node(1) shutil.rmtree( os.path.join( self.nodes[1].datadir, self.chain, "blocks")) 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", self.default_wallet_name, self.wallet_data_filename)) self.start_node(1, extra_args=self.extra_args[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 * 1000000) + 1000000) out = self.nodes[1].rescanblockchain(0, 1) assert_equal(out['start_height'], 0) assert_equal(out['stop_height'], 1) out = self.nodes[1].rescanblockchain(2, 4) assert_equal(out['start_height'], 2) assert_equal(out['stop_height'], 4) out = self.nodes[1].rescanblockchain(3) assert_equal(out['start_height'], 3) assert_equal(out['stop_height'], self.nodes[1].getblockcount()) out = self.nodes[1].rescanblockchain() assert_equal(out['start_height'], 0) assert_equal(out['stop_height'], self.nodes[1].getblockcount()) assert_equal( self.nodes[1].getbalance(), (NUM_HD_ADDS * 1000000) + 1000000) # send a tx and make sure its using the internal chain for the # changeoutput txid = self.nodes[1].sendtoaddress( self.nodes[0].getnewaddress(), 1000000) outs = self.nodes[1].decoderawtransaction( self.nodes[1].gettransaction(txid)['hex'])['vout'] keypath = "" for out in outs: if out['value'] != 1000000: keypath = self.nodes[1].getaddressinfo( out['scriptPubKey']['addresses'][0])['hdkeypath'] if self.options.descriptors: assert_equal(keypath[0:14], "m/44'/1'/0'/1/") else: assert_equal(keypath[0:7], "m/0'/1'") if not self.options.descriptors: # Generate a new HD seed on node 1 and make sure it is set orig_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid'] self.nodes[1].sethdseed() new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid'] assert orig_masterkeyid != new_masterkeyid addr = self.nodes[1].getnewaddress() # Make sure the new address is the first from the keypool assert_equal(self.nodes[1].getaddressinfo( addr)['hdkeypath'], 'm/0\'/0\'/0\'') # Fill keypool with 1 key self.nodes[1].keypoolrefill(1) # Set a new HD seed on node 1 without flushing the keypool new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress()) orig_masterkeyid = new_masterkeyid self.nodes[1].sethdseed(False, new_seed) new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid'] assert orig_masterkeyid != new_masterkeyid addr = self.nodes[1].getnewaddress() assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo( addr)['hdseedid']) # Make sure the new address continues previous keypool assert_equal(self.nodes[1].getaddressinfo( addr)['hdkeypath'], 'm/0\'/0\'/1\'') # Check that the next address is from the new seed self.nodes[1].keypoolrefill(1) next_addr = self.nodes[1].getnewaddress() assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo( next_addr)['hdseedid']) # Make sure the new address is not from previous keypool assert_equal(self.nodes[1].getaddressinfo( next_addr)['hdkeypath'], 'm/0\'/0\'/0\'') assert next_addr != addr # Sethdseed parameter validity assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0) assert_raises_rpc_error(-5, "Invalid private key", self.nodes[1].sethdseed, False, "not_wif") assert_raises_rpc_error( -1, "JSON value is not a boolean as expected", self.nodes[1].sethdseed, "Not_bool") assert_raises_rpc_error( -1, "JSON value is not a string as expected", self.nodes[1].sethdseed, False, True) assert_raises_rpc_error( -5, "Already have this key", self.nodes[1].sethdseed, False, new_seed) assert_raises_rpc_error( -5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress())) self.log.info( 'Test sethdseed restoring with keys outside of the initial keypool') self.generate(self.nodes[0], 10) # 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']) 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') seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress()) origin_rpc.sethdseed(True, seed) self.nodes[1].createwallet(wallet_name='restore', blank=True) restore_rpc = self.nodes[1].get_wallet_rpc('restore') # Set to be the same seed as origin_rpc restore_rpc.sethdseed(True, seed) # Rotate to a new seed, making original `seed` inactive restore_rpc.sethdseed(True) self.nodes[1].createwallet(wallet_name='restore2', blank=True) restore2_rpc = self.nodes[1].get_wallet_rpc('restore2') # Set to be the same seed as origin_rpc restore2_rpc.sethdseed(True, seed) # Rotate to a new seed, making original `seed` inactive restore2_rpc.sethdseed(True) # Check persistence of inactive seed by reloading restore. restore2 # is still loaded to test the case where the wallet is not reloaded restore_rpc.unloadwallet() self.nodes[1].loadwallet('restore') restore_rpc = self.nodes[1].get_wallet_rpc('restore') # Empty origin keypool and get an address that is beyond the # initial keypool origin_rpc.getnewaddress() origin_rpc.getnewaddress() # Last address of initial keypool last_addr = origin_rpc.getnewaddress() # First address beyond initial keypool addr = origin_rpc.getnewaddress() # Check that the restored seed has last_addr but does not have addr info = restore_rpc.getaddressinfo(last_addr) assert_equal(info['ismine'], True) info = restore_rpc.getaddressinfo(addr) assert_equal(info['ismine'], False) info = restore2_rpc.getaddressinfo(last_addr) assert_equal(info['ismine'], True) info = restore2_rpc.getaddressinfo(addr) assert_equal(info['ismine'], False) # Check that the origin seed has addr info = origin_rpc.getaddressinfo(addr) assert_equal(info['ismine'], True) # Send a transaction to addr, which is out of the initial keypool. # The wallet that has set a new seed (restore_rpc) should not # detect this transaction. txid = self.nodes[0].sendtoaddress(addr, 1000000) origin_rpc.sendrawtransaction( self.nodes[0].gettransaction(txid)['hex']) self.generate(self.nodes[0], 1) - self.sync_blocks() origin_rpc.gettransaction(txid) assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid) out_of_kp_txid = txid # Send a transaction to last_addr, which is in the initial keypool. # The wallet that has set a new seed (restore_rpc) should detect # this transaction and generate 3 new keys from the initial seed. # The previous transaction (out_of_kp_txid) should still not be # detected as a rescan is required. txid = self.nodes[0].sendtoaddress(last_addr, 1000000) origin_rpc.sendrawtransaction( self.nodes[0].gettransaction(txid)['hex']) self.generate(self.nodes[0], 1) - self.sync_blocks() origin_rpc.gettransaction(txid) restore_rpc.gettransaction(txid) assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid) restore2_rpc.gettransaction(txid) assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore2_rpc.gettransaction, out_of_kp_txid) # After rescanning, restore_rpc should now see out_of_kp_txid and # generate an additional key. # addr should now be part of restore_rpc and be ismine restore_rpc.rescanblockchain() restore_rpc.gettransaction(out_of_kp_txid) info = restore_rpc.getaddressinfo(addr) assert_equal(info['ismine'], True) restore2_rpc.rescanblockchain() restore2_rpc.gettransaction(out_of_kp_txid) info = restore2_rpc.getaddressinfo(addr) assert_equal(info['ismine'], True) # Check again that 3 keys were derived. # Empty keypool and get an address that is beyond the initial # keypool origin_rpc.getnewaddress() origin_rpc.getnewaddress() last_addr = origin_rpc.getnewaddress() addr = origin_rpc.getnewaddress() # Check that the restored seed has last_addr but does not have addr info = restore_rpc.getaddressinfo(last_addr) assert_equal(info['ismine'], True) info = restore_rpc.getaddressinfo(addr) assert_equal(info['ismine'], False) info = restore2_rpc.getaddressinfo(last_addr) assert_equal(info['ismine'], True) info = restore2_rpc.getaddressinfo(addr) assert_equal(info['ismine'], False) if __name__ == '__main__': WalletHDTest().main() diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index 411989485..d910949f9 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -1,235 +1,234 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test wallet import RPCs. Test rescan behavior of importaddress, importpubkey, importprivkey, and importmulti RPCs with different types of keys and rescan options. In the first part of the test, node 0 creates an address for each type of import RPC call and node 0 sends XEC to it. Then other nodes import the addresses, and the test makes listtransactions and getbalance calls to confirm that the importing node either did or did not execute rescans picking up the send transactions. In the second part of the test, node 0 sends more XEC to each address, and the test makes more listtransactions and getbalance calls to confirm that the importing nodes pick up the new transactions regardless of whether rescans happened previously. """ import collections import enum import itertools import random from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, set_node_times Call = enum.Enum("Call", "single multiaddress multiscript") Data = enum.Enum("Data", "address pub priv") Rescan = enum.Enum("Rescan", "no yes late_timestamp") class Variant(collections.namedtuple("Variant", "call data rescan prune")): """Helper for importing one key and verifying scanned transactions.""" def do_import(self, timestamp): """Call one key import RPC.""" rescan = self.rescan == Rescan.yes assert_equal(self.address["solvable"], True) if self.call == Call.single: if self.data == Data.address: response = self.node.importaddress( address=self.address["address"], label=self.label, rescan=rescan) elif self.data == Data.pub: response = self.node.importpubkey( pubkey=self.address["pubkey"], label=self.label, rescan=rescan) elif self.data == Data.priv: response = self.node.importprivkey( privkey=self.key, label=self.label, rescan=rescan) assert_equal(response, None) elif self.call in (Call.multiaddress, Call.multiscript): request = { "scriptPubKey": { "address": self.address["address"] } if self.call == Call.multiaddress else self.address["scriptPubKey"], "timestamp": timestamp + TIMESTAMP_WINDOW + ( 1 if self.rescan == Rescan.late_timestamp else 0), "pubkeys": [self.address["pubkey"]] if self.data == Data.pub else [], "keys": [self.key] if self.data == Data.priv else [], "label": self.label, "watchonly": self.data != Data.priv } response = self.node.importmulti( requests=[request], options={ "rescan": self.rescan in ( Rescan.yes, Rescan.late_timestamp)}, ) assert_equal(response, [{"success": True}]) def check(self, txid=None, amount=None, confirmation_height=None): """Verify that listtransactions/listreceivedbyaddress return expected values.""" txs = self.node.listtransactions( label=self.label, count=10000, include_watchonly=True) current_height = self.node.getblockcount() assert_equal(len(txs), self.expected_txs) addresses = self.node.listreceivedbyaddress( minconf=0, include_watchonly=True, address_filter=self.address['address']) if self.expected_txs: assert_equal(len(addresses[0]["txids"]), self.expected_txs) if txid is not None: tx, = [tx for tx in txs if tx["txid"] == txid] assert_equal(tx["label"], self.label) assert_equal(tx["address"], self.address["address"]) assert_equal(tx["amount"], amount) assert_equal(tx["category"], "receive") assert_equal(tx["label"], self.label) assert_equal(tx["txid"], txid) assert_equal(tx["confirmations"], 1 + current_height - confirmation_height) assert_equal("trusted" not in tx, True) address, = [ad for ad in addresses if txid in ad["txids"]] assert_equal(address["address"], self.address["address"]) assert_equal(address["amount"], self.expected_balance) assert_equal(address["confirmations"], 1 + current_height - confirmation_height) # Verify the transaction is correctly marked watchonly depending on # whether the transaction pays to an imported public key or # imported private key. The test setup ensures that transaction # inputs will not be from watchonly keys (important because # involvesWatchonly will be true if either the transaction output # or inputs are watchonly). if self.data != Data.priv: assert_equal(address["involvesWatchonly"], True) else: assert_equal("involvesWatchonly" not in address, True) # List of Variants for each way a key or address could be imported. IMPORT_VARIANTS = [Variant(*variants) for variants in itertools.product(Call, Data, Rescan, (False, True))] # List of nodes to import keys to. Half the nodes will have pruning disabled, # half will have it enabled. Different nodes will be used for imports that are # expected to cause rescans, and imports that are not expected to cause # rescans, in order to prevent rescans during later imports picking up # transactions associated with earlier imports. This makes it easier to keep # track of expected balances and transactions. ImportNode = collections.namedtuple("ImportNode", "prune rescan") IMPORT_NODES = [ImportNode(*fields) for fields in itertools.product((False, True), repeat=2)] # Rescans start at the earliest block up to 2 hours before the key timestamp. TIMESTAMP_WINDOW = 2 * 60 * 60 AMOUNT_DUST = 5.46 def get_rand_amount(): r = random.uniform(AMOUNT_DUST, 1000000) return Decimal(str(round(r, 2))) class ImportRescanTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 + len(IMPORT_NODES) self.supports_cli = False self.rpc_timeout = 120 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): 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"] self.add_nodes(self.num_nodes, extra_args=self.extra_args) # Import keys with pruning disabled 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): self.connect_nodes(i, 0) def run_test(self): # Create one transaction on node 0 with a unique amount for # each possible type of wallet import RPC. for i, variant in enumerate(IMPORT_VARIANTS): variant.label = "label {} {}".format(i, variant) variant.address = self.nodes[1].getaddressinfo( self.nodes[1].getnewaddress(label=variant.label)) variant.key = self.nodes[1].dumpprivkey(variant.address["address"]) variant.initial_amount = get_rand_amount() variant.initial_txid = self.nodes[0].sendtoaddress( variant.address["address"], variant.initial_amount) # Generate one block for each send self.generate(self.nodes[0], 1) variant.confirmation_height = self.nodes[0].getblockcount() variant.timestamp = self.nodes[0].getblockheader( self.nodes[0].getbestblockhash())["time"] # Generate a block further in the future (past the rescan window). assert_equal(self.nodes[0].getrawmempool(), []) set_node_times(self.nodes, self.nodes[0].getblockheader( self.nodes[0].getbestblockhash())["time"] + TIMESTAMP_WINDOW + 1) self.generate(self.nodes[0], 1) - self.sync_all() # For each variation of wallet key import, invoke the import RPC and # check the results from getbalance and listtransactions. for variant in IMPORT_VARIANTS: self.log.info('Run import for variant {}'.format(variant)) expect_rescan = variant.rescan == Rescan.yes variant.node = self.nodes[2 + IMPORT_NODES.index( ImportNode(variant.prune, expect_rescan))] variant.do_import(variant.timestamp) if expect_rescan: variant.expected_balance = variant.initial_amount variant.expected_txs = 1 variant.check(variant.initial_txid, variant.initial_amount, variant.confirmation_height) else: variant.expected_balance = 0 variant.expected_txs = 0 variant.check() # Create new transactions sending to each address. for i, variant in enumerate(IMPORT_VARIANTS): variant.sent_amount = get_rand_amount() variant.sent_txid = self.nodes[0].sendtoaddress( variant.address["address"], variant.sent_amount) # Generate one block for each send self.generate(self.nodes[0], 1) variant.confirmation_height = self.nodes[0].getblockcount() assert_equal(self.nodes[0].getrawmempool(), []) self.sync_all() # Check the latest results from getbalance and listtransactions. for variant in IMPORT_VARIANTS: self.log.info('Run check for variant {}'.format(variant)) variant.expected_balance += variant.sent_amount variant.expected_txs += 1 variant.check(variant.sent_txid, variant.sent_amount, variant.confirmation_height) if __name__ == "__main__": ImportRescanTest().main() diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 8ba05a07e..7929789a2 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -1,496 +1,493 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the importdescriptors RPC. Test importdescriptors by generating keys on node0, importing the corresponding descriptors on node1 and then testing the address info for the different address variants. - `get_generate_key()` is called to generate keys and return the privkeys, pubkeys and all variants of scriptPubKey and address. - `test_importdesc()` is called to send an importdescriptors call to node1, test success, and (if unsuccessful) test the error code and error message returned. - `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.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, find_vout_for_address, ) from test_framework.wallet_util import get_generate_key, test_address class ImportDescriptorsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [[], ["-keypool=5"]] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") self.setup_clean_chain = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() def test_importdesc(self, req, success, error_code=None, error_message=None, warnings=None, wallet=None): """Run importdescriptors and assert success""" if warnings is None: warnings = [] wrpc = self.nodes[1].get_wallet_rpc('w1') if wallet is not None: wrpc = wallet result = wrpc.importdescriptors([req]) observed_warnings = [] if 'warnings' in result[0]: observed_warnings = result[0]['warnings'] assert_equal( "\n".join( sorted(warnings)), "\n".join( sorted(observed_warnings))) assert_equal(result[0]['success'], success) if error_code is not None: assert_equal(result[0]['error']['code'], error_code) assert_equal(result[0]['error']['message'], error_message) def run_test(self): self.log.info('Setting up wallets') self.nodes[0].createwallet( wallet_name='w0', disable_private_keys=False) w0 = self.nodes[0].get_wallet_rpc('w0') self.nodes[1].createwallet( wallet_name='w1', disable_private_keys=True, blank=True, descriptors=True) w1 = self.nodes[1].get_wallet_rpc('w1') assert_equal(w1.getwalletinfo()['keypoolsize'], 0) self.nodes[1].createwallet( wallet_name="wpriv", disable_private_keys=False, blank=True, descriptors=True) wpriv = self.nodes[1].get_wallet_rpc("wpriv") assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0) self.log.info('Mining coins') self.generatetoaddress(self.nodes[0], 101, w0.getnewaddress()) # RPC importdescriptors ----------------------------------------------- # # Test import fails if no descriptor present key = get_generate_key() self.log.info("Import should fail if a descriptor is not provided") self.test_importdesc({"timestamp": "now"}, success=False, error_code=-8, error_message='Descriptor not found.') # # Test importing of a P2PKH descriptor key = get_generate_key() self.log.info("Should import a p2pkh descriptor") self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), "timestamp": "now", "label": "Descriptor import test"}, success=True) test_address(w1, key.p2pkh_addr, solvable=True, ismine=True, labels=["Descriptor import test"]) assert_equal(w1.getwalletinfo()['keypoolsize'], 0) self.log.info("Internal addresses cannot have labels") self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), "timestamp": "now", "internal": True, "label": "Descriptor import test"}, success=False, 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) assert_equal(w1.getwalletinfo()['keypoolsize'], 0) test_address(w1, key.p2pkh_addr, ismine=True, solvable=True) # Check persistence of data and that loading works correctly w1.unloadwallet() self.nodes[1].loadwallet('w1') test_address(w1, key.p2pkh_addr, ismine=True, solvable=True) # # Test importing of a multisig descriptor key1 = get_generate_key() key2 = get_generate_key() self.log.info("Should import a 1-of-2 bare multisig from descriptor") self.test_importdesc({"desc": descsum_create("multi(1," + key1.pubkey + "," + key2.pubkey + ")"), "timestamp": "now"}, success=True) self.log.info( "Should not treat individual keys from the imported bare multisig as watchonly") test_address(w1, key1.p2pkh_addr, ismine=False) # # Test ranged descriptors xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg" xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H" addresses = [ "2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] # hdkeypath=m/0'/0'/0' and 1' # wpkh subscripts corresponding to the above addresses addresses += ["ecregtest:prvn9ycvgr5atuyh49sua3mapskh2mnnzg7t9yp6dt", "ecregtest:pp3n087yx0njv2e5wcvltahfxqst7l66rutz8ceeat"] desc = "sh(pkh(" + xpub + "/0/0/*" + "))" self.log.info("Ranged descriptors cannot have labels") self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 100], "label": "test"}, success=False, error_code=-8, error_message='Ranged descriptors should not have a label') self.log.info("Private keys required for private keys enabled wallet") self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 100]}, success=False, error_code=-4, error_message='Cannot import descriptor without private keys to a wallet with private keys enabled', wallet=wpriv) self.log.info( "Ranged descriptor import should warn without a specified range") self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now"}, success=True, warnings=['Range not given, using default keypool range']) assert_equal(w1.getwalletinfo()['keypoolsize'], 0) # # Test importing of a ranged descriptor with xpriv self.log.info( "Should not import a ranged descriptor that includes xpriv into a watch-only wallet") desc = "sh(pkh(" + xpriv + "/0'/0'/*'" + "))" self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": 1}, 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("pkh(" + 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, ismine=False, solvable=False) self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": -1}, success=False, error_code=-8, error_message='End of range is too high') self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [-1, 10]}, success=False, error_code=-8, error_message='Range should be greater or equal than 0') self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]}, success=False, error_code=-8, error_message='End of range is too high') self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [2, 1]}, success=False, error_code=-8, error_message='Range specified as [begin,end] must not have begin after end') self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]}, success=False, error_code=-8, error_message='Range is too large') # Make sure ranged imports import keys in order w1 = self.nodes[1].get_wallet_rpc('w1') self.log.info('Key ranges should be imported in order') xpub = "tpubDAXcJ7s7ZwicqjprRaEWdPoHKrCS215qxGYxpusRLLmJuT69ZSicuGdSfyvyKpvUNYBW1s2U3NSrT6vrCYB9e6nZUEvrqnwXPF8ArTCRXMY" addresses = [ 'ecregtest:qp0v86h53rc92hjrlpwzpjtdlgzsxu25svv6g40fpl', # m/0'/0'/0 'ecregtest:qqasy0zlkdleqt4pkn8fs4ehm5gnnz6qpgdcpt90fq', # m/0'/0'/1 'ecregtest:qp0sp4wlhctvprqvdt2dgvqcfdjssu04xgey0l3syw', # m/0'/0'/2 'ecregtest:qrhn24tegn04cptfv4ldhtkduxq55zcwrycjfdj9vr', # m/0'/0'/3 'ecregtest:qzpqhett2uwltq803vrxv7zkqhft5vsnmcjeh50v0p', # m/0'/0'/4 ] self.test_importdesc({'desc': descsum_create('sh(pkh([abcdef12/0h/0h]' + xpub + '/*))'), 'active': True, 'range': [0, 2], 'timestamp': 'now' }, success=True) self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'), 'active': True, 'range': [0, 2], 'timestamp': 'now' }, success=True) assert_equal(w1.getwalletinfo()['keypoolsize'], 5) for i, expected_addr in enumerate(addresses): pkh_addr = w1.getnewaddress('') assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress) assert_equal(pkh_addr, expected_addr) pkh_addr_info = w1.getaddressinfo(pkh_addr) assert_equal(pkh_addr_info['desc'][:22], 'pkh([12345678/0\'/0\'/{}]'.format(i)) # After retrieving a key, we don't refill the keypool again, so # it's one less for each address type assert_equal(w1.getwalletinfo()['keypoolsize'], 4) w1.keypoolrefill() assert_equal(w1.getwalletinfo()['keypoolsize'], 5) # Check active=False default self.log.info('Check imported descriptors are not active by default') self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'), 'range': [0, 2], 'timestamp': 'now', 'internal': True }, success=True) assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress) # # Test importing a descriptor containing a WIF private key wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh" address = "ecregtest:ppn85zpvym8cdccmgw8km6e48jfhnpa435h3hkyfd6" desc = "sh(pkh(" + wif_priv + "))" self.log.info( "Should import a descriptor with a WIF private key as spendable") self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now"}, success=True, wallet=wpriv) test_address(wpriv, address, solvable=True, ismine=True) txid = w0.sendtoaddress(address, 49999996.00) self.generatetoaddress(self.nodes[0], 6, w0.getnewaddress()) - self.sync_blocks() tx = wpriv.createrawtransaction([{"txid": txid, "vout": 0}], { w0.getnewaddress(): 49999000}) signed_tx = wpriv.signrawtransactionwithwallet(tx) w1.sendrawtransaction(signed_tx['hex']) # Make sure that we can use import and use multisig as addresses self.log.info( 'Test that multisigs can be imported, signed for, and getnewaddress\'d') self.nodes[1].createwallet( wallet_name="wmulti_priv", disable_private_keys=False, blank=True, descriptors=True) wmulti_priv = self.nodes[1].get_wallet_rpc("wmulti_priv") assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 0) self.test_importdesc({"desc": "sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/0h/0h/*))#f5nqn4ax", "active": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_priv) self.test_importdesc({"desc": "sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/1h/0h/*))#m4e4s5de", "active": True, "internal": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_priv) # Range end (1000) is inclusive, so 1001 addresses generated assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) addr = wmulti_priv.getnewaddress('') # Derived at m/84'/0'/0'/0 assert_equal( addr, 'ecregtest:pzkcf26dw7np58jcspnpxaupgz9csnc3wsf5wdh2a3') change_addr = wmulti_priv.getrawchangeaddress() assert_equal( change_addr, 'ecregtest:prnkfg7pxe3kpyv3l4v00ft6q3sfseag7vnva0k49n') assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000) txid = w0.sendtoaddress(addr, 10000000) self.generate(self.nodes[0], 6) - self.sync_all() self.nodes[1].createwallet( wallet_name="wmulti_pub", disable_private_keys=True, blank=True, descriptors=True) wmulti_pub = self.nodes[1].get_wallet_rpc("wmulti_pub") assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 0) self.test_importdesc({"desc": "sh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))#x75vpsak", "active": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_pub) self.test_importdesc({"desc": "sh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))#v0t48ucu", "active": True, "internal": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_pub) # The first one was already consumed by previous import and is detected # as used assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) addr = wmulti_pub.getnewaddress('') # Derived at m/84'/0'/0'/1 assert_equal( addr, 'ecregtest:pr5xql8r03jp5dvrep22dns59vf7hhykr5nmaucy2h') change_addr = wmulti_pub.getrawchangeaddress() assert_equal( change_addr, 'ecregtest:prnkfg7pxe3kpyv3l4v00ft6q3sfseag7vnva0k49n') assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999) txid = w0.sendtoaddress(addr, 10000000) vout = find_vout_for_address(self.nodes[0], txid, addr) self.generate(self.nodes[0], 6) - self.sync_all() assert_equal(wmulti_pub.getbalance(), wmulti_priv.getbalance()) # Make sure that descriptor wallets containing multiple xpubs in a # single descriptor load correctly wmulti_pub.unloadwallet() self.nodes[1].loadwallet('wmulti_pub') self.log.info("Multisig with distributed keys") self.nodes[1].createwallet( wallet_name="wmulti_priv1", descriptors=True) wmulti_priv1 = self.nodes[1].get_wallet_rpc("wmulti_priv1") res = wmulti_priv1.importdescriptors([ { "desc": descsum_create("sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"), "active": True, "range": 1000, "next_index": 0, "timestamp": "now" }, { "desc": descsum_create("sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"), "active": True, "internal": True, "range": 1000, "next_index": 0, "timestamp": "now" }]) assert_equal(res[0]['success'], True) assert_equal( res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors') assert_equal(res[1]['success'], True) assert_equal( res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors') self.nodes[1].createwallet( wallet_name='wmulti_priv2', blank=True, descriptors=True) wmulti_priv2 = self.nodes[1].get_wallet_rpc('wmulti_priv2') res = wmulti_priv2.importdescriptors([ { "desc": descsum_create("sh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"), "active": True, "range": 1000, "next_index": 0, "timestamp": "now" }, { "desc": descsum_create("sh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"), "active": True, "internal": True, "range": 1000, "next_index": 0, "timestamp": "now" }]) assert_equal(res[0]['success'], True) assert_equal( res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors') assert_equal(res[1]['success'], True) assert_equal( res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors') rawtx = self.nodes[1].createrawtransaction( [{'txid': txid, 'vout': vout}], {w0.getnewaddress(): 9999000}) tx_signed_1 = wmulti_priv1.signrawtransactionwithwallet(rawtx) assert_equal(tx_signed_1['complete'], False) tx_signed_2 = wmulti_priv2.signrawtransactionwithwallet( tx_signed_1['hex']) assert_equal(tx_signed_2['complete'], True) self.nodes[1].sendrawtransaction(tx_signed_2['hex']) self.log.info("Combo descriptors cannot be active") self.test_importdesc({"desc": descsum_create("combo(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"), "active": True, "range": 1, "timestamp": "now"}, success=False, error_code=-4, error_message="Combo descriptors cannot be set to active") self.log.info("Descriptors with no type cannot be active") self.test_importdesc({"desc": descsum_create("pk(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"), "active": True, "range": 1, "timestamp": "now"}, success=True, warnings=["Unknown output type, cannot set descriptor to active."]) if __name__ == '__main__': ImportDescriptorsTest().main() diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 6a99640e3..607a6d384 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -1,136 +1,134 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2016 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 the importprunedfunds and removeprunedfunds RPCs.""" from decimal import Decimal from test_framework.address import key_to_p2pkh 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): self.setup_clean_chain = True self.num_nodes = 2 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): self.log.info("Mining blocks...") self.generate(self.nodes[0], 101) - self.sync_all() - # address address1 = self.nodes[0].getnewaddress() # pubkey address2 = self.nodes[0].getnewaddress() # privkey eckey = ECKey() eckey.generate() address3_privkey = bytes_to_wif(eckey.get_bytes()) address3 = key_to_p2pkh(eckey.get_pubkey().get_bytes()) self.nodes[0].importprivkey(address3_privkey) # Check only one address address_info = self.nodes[0].getaddressinfo(address1) assert_equal(address_info['ismine'], True) self.sync_all() # Node 1 sync test assert_equal(self.nodes[1].getblockcount(), 101) # Address Test - before import address_info = self.nodes[1].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'], False) assert_equal(address_info['ismine'], False) address_info = self.nodes[1].getaddressinfo(address3) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], False) # Send funds to self txnid1 = self.nodes[0].sendtoaddress(address1, 100000) self.generate(self.nodes[0], 1) rawtxn1 = self.nodes[0].gettransaction(txnid1)['hex'] proof1 = self.nodes[0].gettxoutproof([txnid1]) txnid2 = self.nodes[0].sendtoaddress(address2, 50000) self.generate(self.nodes[0], 1) rawtxn2 = self.nodes[0].gettransaction(txnid2)['hex'] proof2 = self.nodes[0].gettxoutproof([txnid2]) txnid3 = self.nodes[0].sendtoaddress(address3, 25000) self.generate(self.nodes[0], 1) rawtxn3 = self.nodes[0].gettransaction(txnid3)['hex'] proof3 = self.nodes[0].gettxoutproof([txnid3]) self.sync_all() # Import with no affiliated address assert_raises_rpc_error( -5, "No addresses", self.nodes[1].importprunedfunds, rawtxn1, proof1) balance1 = self.nodes[1].getbalance() assert_equal(balance1, Decimal(0)) # Import with affiliated address with no rescan 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 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('25000')) # Addresses Test - after import address_info = w1.getaddressinfo(address1) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], False) 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.", w1.removeprunedfunds, txnid1) assert not [tx for tx in w1.listtransactions( include_watchonly=True) if tx['txid'] == txnid1] wwatch.removeprunedfunds(txnid2) assert not [tx for tx in wwatch.listtransactions( include_watchonly=True) if tx['txid'] == txnid2] 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_topup.py b/test/functional/wallet_keypool_topup.py index 9ac0a9cf5..efadbd03d 100755 --- a/test/functional/wallet_keypool_topup.py +++ b/test/functional/wallet_keypool_topup.py @@ -1,80 +1,79 @@ #!/usr/bin/env python3 # Copyright (c) 2017-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test HD Wallet keypool restore function. Two nodes. Node1 is under test. Node0 is providing transactions and generating blocks. - Start node1, shutdown and backup wallet. - Generate 110 keys (enough to drain the keypool). Store key 90 (in the initial keypool) and key 110 (beyond the initial keypool). Send funds to key 90 and key 110. - Stop node1, clear the datadir, move wallet file back into the datadir and restart node1. - connect node1 to node0. Verify that they sync and node1 receives its funds.""" import os import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class KeypoolRestoreTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [[], ['-keypool=100']] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): 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.generate(self.nodes[0], 101) self.log.info("Make backup of wallet") self.stop_node(1) shutil.copyfile(wallet_path, wallet_backup_path) self.start_node(1, self.extra_args[1]) self.connect_nodes(0, 1) self.log.info("Generate keys for wallet") for _ in range(90): addr_oldpool = self.nodes[1].getnewaddress() for _ in range(20): addr_extpool = self.nodes[1].getnewaddress() self.log.info("Send funds to wallet") self.nodes[0].sendtoaddress(addr_oldpool, 10000000) self.generate(self.nodes[0], 1) self.nodes[0].sendtoaddress(addr_extpool, 5000000) self.generate(self.nodes[0], 1) - self.sync_blocks() self.log.info("Restart node with wallet backup") self.stop_node(1) shutil.copyfile(wallet_backup_path, wallet_path) self.start_node(1, self.extra_args[1]) self.connect_nodes(0, 1) self.sync_all() self.log.info("Verify keypool is restored and balance is correct") assert_equal(self.nodes[1].getbalance(), 15000000) assert_equal(self.nodes[1].listtransactions() [0]['category'], "receive") # Check that we have marked all keys up to the used keypool key as used if self.options.descriptors: assert_equal( self.nodes[1].getaddressinfo( self.nodes[1].getnewaddress())['hdkeypath'], "m/44'/1'/0'/0/110") else: assert_equal( self.nodes[1].getaddressinfo( self.nodes[1].getnewaddress())['hdkeypath'], "m/0'/0'/110'") if __name__ == '__main__': KeypoolRestoreTest().main() diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py index 8c8e61126..cb70c7cec 100755 --- a/test/functional/wallet_listreceivedby.py +++ b/test/functional/wallet_listreceivedby.py @@ -1,196 +1,191 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listreceivedbyaddress RPC.""" from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_array_result, assert_equal, assert_raises_rpc_error, ) from test_framework.wallet_util import test_address class ReceivedByTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 # whitelist peers to speed up tx relay / mempool sync self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_cli() def run_test(self): # Generate block to get out of IBD self.generate(self.nodes[0], 1) - self.sync_blocks() # save the number of coinbase reward addresses so far num_cb_reward_addresses = len( self.nodes[1].listreceivedbyaddress( minconf=0, include_empty=True, include_watchonly=True)) self.log.info("listreceivedbyaddress Test") # Send from node 0 to 1 addr = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(addr, 100000) self.sync_all() # Check not listed in listreceivedbyaddress because has 0 confirmations assert_array_result(self.nodes[1].listreceivedbyaddress(), {"address": addr}, {}, True) # Bury Tx under 10 block so it will be returned by # listreceivedbyaddress self.generate(self.nodes[1], 10) - self.sync_all() assert_array_result(self.nodes[1].listreceivedbyaddress(), {"address": addr}, {"address": addr, "label": "", "amount": Decimal("100000"), "confirmations": 10, "txids": [txid, ]}) # With min confidence < 10 assert_array_result(self.nodes[1].listreceivedbyaddress(5), {"address": addr}, {"address": addr, "label": "", "amount": Decimal("100000"), "confirmations": 10, "txids": [txid, ]}) # With min confidence > 10, should not find Tx assert_array_result(self.nodes[1].listreceivedbyaddress(11), { "address": addr}, {}, True) # Empty Tx empty_addr = self.nodes[1].getnewaddress() assert_array_result(self.nodes[1].listreceivedbyaddress(0, True), {"address": empty_addr}, {"address": empty_addr, "label": "", "amount": 0, "confirmations": 0, "txids": []}) # Test Address filtering # Only on addr expected = {"address": addr, "label": "", "amount": Decimal( "100000"), "confirmations": 10, "txids": [txid, ]} res = self.nodes[1].listreceivedbyaddress( minconf=0, include_empty=True, include_watchonly=True, address_filter=addr) assert_array_result(res, {"address": addr}, expected) assert_equal(len(res), 1) # Test for regression on CLI calls with address string (#14173) cli_res = self.nodes[1].cli.listreceivedbyaddress(0, True, True, addr) assert_array_result(cli_res, {"address": addr}, expected) assert_equal(len(cli_res), 1) # Error on invalid address assert_raises_rpc_error(-4, "address_filter parameter was invalid", self.nodes[1].listreceivedbyaddress, minconf=0, include_empty=True, include_watchonly=True, address_filter="bamboozling") # Another address receive money res = self.nodes[1].listreceivedbyaddress(0, True, True) # Right now 2 entries assert_equal(len(res), 2 + num_cb_reward_addresses) other_addr = self.nodes[1].getnewaddress() txid2 = self.nodes[0].sendtoaddress(other_addr, 100000) self.generate(self.nodes[0], 1) - self.sync_all() # Same test as above should still pass expected = {"address": addr, "label": "", "amount": Decimal( "100000"), "confirmations": 11, "txids": [txid, ]} res = self.nodes[1].listreceivedbyaddress(0, True, True, addr) assert_array_result(res, {"address": addr}, expected) assert_equal(len(res), 1) # Same test as above but with other_addr should still pass expected = {"address": other_addr, "label": "", "amount": Decimal( "100000"), "confirmations": 1, "txids": [txid2, ]} res = self.nodes[1].listreceivedbyaddress(0, True, True, other_addr) assert_array_result(res, {"address": other_addr}, expected) assert_equal(len(res), 1) # Should be two entries though without filter res = self.nodes[1].listreceivedbyaddress(0, True, True) # Became 3 entries assert_equal(len(res), 3 + num_cb_reward_addresses) # Not on random addr # note on node[0]! just a random addr other_addr = self.nodes[0].getnewaddress() res = self.nodes[1].listreceivedbyaddress(0, True, True, other_addr) assert_equal(len(res), 0) self.log.info("getreceivedbyaddress Test") # Send from node 0 to 1 addr = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(addr, 100000) self.sync_all() # Check balance is 0 because of 0 confirmations balance = self.nodes[1].getreceivedbyaddress(addr) assert_equal(balance, Decimal("0.0")) # Check balance is 0.1 balance = self.nodes[1].getreceivedbyaddress(addr, 0) assert_equal(balance, Decimal("100000")) # Bury Tx under 10 block so it will be returned by the default # getreceivedbyaddress self.generate(self.nodes[1], 10) - self.sync_all() balance = self.nodes[1].getreceivedbyaddress(addr) assert_equal(balance, Decimal("100000")) # Trying to getreceivedby for an address the wallet doesn't own should # return an error assert_raises_rpc_error(-4, "Address not found in wallet", self.nodes[0].getreceivedbyaddress, addr) self.log.info("listreceivedbylabel + getreceivedbylabel Test") # set pre-state label = '' address = self.nodes[1].getnewaddress() test_address(self.nodes[1], address, labels=[label]) received_by_label_json = [ r for r in self.nodes[1].listreceivedbylabel() if r["label"] == label][0] balance_by_label = self.nodes[1].getreceivedbylabel(label) txid = self.nodes[0].sendtoaddress(addr, 100000) self.sync_all() # listreceivedbylabel should return received_by_label_json because of 0 # confirmations assert_array_result(self.nodes[1].listreceivedbylabel(), {"label": label}, received_by_label_json) # getreceivedbyaddress should return same balance because of 0 # confirmations balance = self.nodes[1].getreceivedbylabel(label) assert_equal(balance, balance_by_label) self.generate(self.nodes[1], 10) - self.sync_all() # listreceivedbylabel should return updated received list assert_array_result(self.nodes[1].listreceivedbylabel(), {"label": label}, {"label": received_by_label_json["label"], "amount": (received_by_label_json["amount"] + Decimal("100000"))}) # getreceivedbylabel should return updated receive total balance = self.nodes[1].getreceivedbylabel(label) assert_equal(balance, balance_by_label + Decimal("100000")) # Create a new label named "mynewlabel" that has a 0 balance address = self.nodes[1].getnewaddress() self.nodes[1].setlabel(address, "mynewlabel") received_by_label_json = [r for r in self.nodes[1].listreceivedbylabel( 0, True) if r["label"] == "mynewlabel"][0] # Test includeempty of listreceivedbylabel assert_equal(received_by_label_json["amount"], Decimal("0.0")) # Test getreceivedbylabel for 0 amount labels balance = self.nodes[1].getreceivedbylabel("mynewlabel") assert_equal(balance, Decimal("0.0")) if __name__ == '__main__': ReceivedByTest().main() diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index c1f5d06b2..c92373de0 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -1,350 +1,348 @@ #!/usr/bin/env python3 # Copyright (c) 2017-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listsinceblock RPC.""" from test_framework import cashaddr from test_framework.key import ECKey from test_framework.script import hash160 from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_array_result, assert_equal, assert_raises_rpc_error, ) from test_framework.wallet_util import bytes_to_wif class ListSinceBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") def skip_test_if_missing_module(self): self.skip_if_no_wallet() 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) self.connect_nodes(1, 2) self.generate(self.nodes[2], 101) - self.sync_all() self.test_no_blockhash() self.test_invalid_blockhash() self.test_reorg() self.test_double_spend() self.test_double_send() self.test_targetconfirmations() def test_no_blockhash(self): self.log.info("Test no blockhash") txid = self.nodes[2].sendtoaddress( self.nodes[0].getnewaddress(), 1000000) blockhash, = self.generate(self.nodes[2], 1) blockheight = self.nodes[2].getblockheader(blockhash)['height'] txs = self.nodes[0].listtransactions() assert_array_result(txs, {"txid": txid}, { "category": "receive", "amount": 1000000, "blockhash": blockhash, "blockheight": blockheight, "confirmations": 1, }) assert_equal( self.nodes[0].listsinceblock(), {"lastblock": blockhash, "removed": [], "transactions": txs}) assert_equal( self.nodes[0].listsinceblock(""), {"lastblock": blockhash, "removed": [], "transactions": txs}) def test_invalid_blockhash(self): self.log.info("Test invalid blockhash") assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock, "42759cde25462784395a337460bde75f58e73d3f08bd31fdc3507cbac856a2c4") assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock, "0000000000000000000000000000000000000000000000000000000000000000") assert_raises_rpc_error(-8, "blockhash must be of length 64 (not 11, for 'invalid-hex')", self.nodes[0].listsinceblock, "invalid-hex") assert_raises_rpc_error(-8, "blockhash must be hexadecimal string (not 'Z000000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].listsinceblock, "Z000000000000000000000000000000000000000000000000000000000000000") def test_targetconfirmations(self): ''' This tests when the value of target_confirmations exceeds the number of blocks in the main chain. In this case, the genesis block hash should be given for the `lastblock` property. If target_confirmations is < 1, then a -8 invalid parameter error is thrown. ''' self.log.info("Test target_confirmations") blockhash, = self.generate(self.nodes[2], 1) blockheight = self.nodes[2].getblockheader(blockhash)['height'] assert_equal( self.nodes[0].getblockhash(0), self.nodes[0].listsinceblock(blockhash, blockheight + 1)['lastblock']) assert_equal( self.nodes[0].getblockhash(0), self.nodes[0].listsinceblock(blockhash, blockheight + 1000)['lastblock']) assert_raises_rpc_error(-8, "Invalid parameter", self.nodes[0].listsinceblock, blockhash, 0) def test_reorg(self): ''' `listsinceblock` did not behave correctly when handed a block that was no longer in the main chain: ab0 / \ aa1 [tx0] bb1 | | aa2 bb2 | | aa3 bb3 | bb4 Consider a client that has only seen block `aa3` above. It asks the node to `listsinceblock aa3`. But at some point prior the main chain switched to the bb chain. Previously: listsinceblock would find height=4 for block aa3 and compare this to height=5 for the tip of the chain (bb4). It would then return results restricted to bb3-bb4. Now: listsinceblock finds the fork at ab0 and returns results in the range bb1-bb4. This test only checks that [tx0] is present. ''' self.log.info("Test reorg") # Split network into two self.split_network() # send to nodes[0] from nodes[2] senttx = self.nodes[2].sendtoaddress( self.nodes[0].getnewaddress(), 1000000) # generate on both sides nodes1_last_blockhash = self.generate( self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1] nodes2_first_blockhash = self.generate( self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0] self.log.debug( "nodes[1] last blockhash = {}".format(nodes1_last_blockhash)) self.log.debug( "nodes[2] first blockhash = {}".format(nodes2_first_blockhash)) self.join_network() # listsinceblock(nodes1_last_blockhash) should now include tx as seen from nodes[0] # and return the block height which listsinceblock now exposes since # rABC6098a1cb2b25. transactions = self.nodes[0].listsinceblock( nodes1_last_blockhash)['transactions'] found = next(tx for tx in transactions if tx['txid'] == senttx) assert_equal( found['blockheight'], self.nodes[0].getblockheader(nodes2_first_blockhash)['height']) def test_double_spend(self): ''' This tests the case where the same UTXO is spent twice on two separate blocks as part of a reorg. ab0 / \ aa1 [tx1] bb1 [tx2] | | aa2 bb2 | | aa3 bb3 | bb4 Problematic case: 1. User 1 receives XEC in tx1 from utxo1 in block aa1. 2. User 2 receives XEC in tx2 from utxo1 (same) in block bb1 3. User 1 sees 2 confirmations at block aa3. 4. Reorg into bb chain. 5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now invalidated. Currently the solution to this is to detect that a reorg'd block is asked for in listsinceblock, and to iterate back over existing blocks up until the fork point, and to include all transactions that relate to the node wallet. ''' self.log.info("Test double spend") self.sync_all() # share utxo between nodes[1] and nodes[2] eckey = ECKey() eckey.generate() privkey = bytes_to_wif(eckey.get_bytes()) address = cashaddr.encode_full("ecregtest", cashaddr.PUBKEY_TYPE, hash160(eckey.get_pubkey().get_bytes())) self.nodes[2].sendtoaddress(address, 10_000_000) self.generate(self.nodes[2], 6) - self.sync_all() self.nodes[2].importprivkey(privkey) utxos = self.nodes[2].listunspent() 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 = '{:.2f}'.format(float(utxo['amount']) - 1000300.00) recipient_dict = { self.nodes[0].getnewaddress(): 1000000, self.nodes[1].getnewaddress(): change, } utxo_dicts = [{ 'txid': utxo['txid'], 'vout': utxo['vout'], }] txid1 = self.nodes[1].sendrawtransaction( self.nodes[1].signrawtransactionwithwallet( self.nodes[1].createrawtransaction(utxo_dicts, recipient_dict))['hex']) # send from nodes[2] using utxo to nodes[3] recipient_dict2 = { self.nodes[3].getnewaddress(): 1000000, self.nodes[2].getnewaddress(): change, } self.nodes[2].sendrawtransaction( self.nodes[2].signrawtransactionwithwallet( self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict2))['hex']) # generate on both sides lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] self.generate(self.nodes[2], 4, sync_fun=self.no_op) self.join_network() self.sync_all() # gettransaction should work for txid1 assert self.nodes[0].gettransaction( txid1)['txid'] == txid1, "gettransaction failed to find txid1" # listsinceblock(lastblockhash) should now include txid1, as seen from # nodes[0] lsbres = self.nodes[0].listsinceblock(lastblockhash) assert any(tx['txid'] == txid1 for tx in lsbres['removed']) # but it should not include 'removed' if include_removed=false lsbres2 = self.nodes[0].listsinceblock( blockhash=lastblockhash, include_removed=False) assert 'removed' not in lsbres2 def test_double_send(self): ''' This tests the case where the same transaction is submitted twice on two separate blocks as part of a reorg. The former will vanish and the latter will appear as the true transaction (with confirmations dropping as a result). ab0 / \ aa1 [tx1] bb1 | | aa2 bb2 | | aa3 bb3 [tx1] | bb4 Asserted: 1. tx1 is listed in listsinceblock. 2. It is included in 'removed' as it was removed, even though it is now present in a different block. 3. It is listed with a confirmation count of 2 (bb3, bb4), not 3 (aa1, aa2, aa3). ''' self.log.info("Test double send") self.sync_all() # Split network into two self.split_network() # create and sign a transaction utxos = self.nodes[2].listunspent() utxo = utxos[0] change = '{:.2f}'.format(float(utxo['amount']) - 1000300.00) recipient_dict = { self.nodes[0].getnewaddress(): 1000000, self.nodes[2].getnewaddress(): change, } utxo_dicts = [{ 'txid': utxo['txid'], 'vout': utxo['vout'], }] signedtxres = self.nodes[2].signrawtransactionwithwallet( self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict)) assert signedtxres['complete'] signedtx = signedtxres['hex'] # send from nodes[1]; this will end up in aa1 txid1 = self.nodes[1].sendrawtransaction(signedtx) # generate bb1-bb2 on right side self.generate(self.nodes[2], 2, sync_fun=self.no_op) # send from nodes[2]; this will end up in bb3 txid2 = self.nodes[2].sendrawtransaction(signedtx) assert_equal(txid1, txid2) # generate on both sides lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] self.generate(self.nodes[2], 2, sync_fun=self.no_op) self.join_network() self.sync_all() # gettransaction should work for txid1 tx1 = self.nodes[0].gettransaction(txid1) assert_equal( tx1['blockheight'], self.nodes[0].getblockheader( tx1['blockhash'])['height']) # listsinceblock(lastblockhash) should now include txid1 in transactions # as well as in removed lsbres = self.nodes[0].listsinceblock(lastblockhash) assert any(tx['txid'] == txid1 for tx in lsbres['transactions']) assert any(tx['txid'] == txid1 for tx in lsbres['removed']) # find transaction and ensure confirmations is valid for tx in lsbres['transactions']: if tx['txid'] == txid1: assert_equal(tx['confirmations'], 2) # the same check for the removed array; confirmations should STILL be 2 for tx in lsbres['removed']: if tx['txid'] == txid1: assert_equal(tx['confirmations'], 2) if __name__ == '__main__': ListSinceBlockTest().main() diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 1ce8a3d2e..774412259 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -1,120 +1,119 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listtransactions API.""" from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_array_result, assert_equal class ListTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 # This test isn't testing txn relay/timing, so set whitelist on the # peers for instant txn relay. This speeds up the test run time 2-3x. self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Leave IBD self.generate(self.nodes[0], 1) # Simple send, 0 to 1: txid = self.nodes[0].sendtoaddress( self.nodes[1].getnewaddress(), 100000) self.sync_all() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, {"category": "send", "amount": Decimal("-100000"), "confirmations": 0}) assert_array_result(self.nodes[1].listtransactions(), {"txid": txid}, {"category": "receive", "amount": Decimal("100000"), "confirmations": 0}) # mine a block, confirmations should change: blockhash = self.generate(self.nodes[0], 1)[0] blockheight = self.nodes[0].getblockheader(blockhash)['height'] assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, {"category": "send", "amount": Decimal("-100000"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight}) assert_array_result(self.nodes[1].listtransactions(), {"txid": txid}, {"category": "receive", "amount": Decimal("100000"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight}) # send-to-self: txid = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), 200000) assert_array_result(self.nodes[0].listtransactions(), {"txid": txid, "category": "send"}, {"amount": Decimal("-200000")}) assert_array_result(self.nodes[0].listtransactions(), {"txid": txid, "category": "receive"}, {"amount": Decimal("200000")}) # sendmany from node1: twice to self, twice to node2: send_to = {self.nodes[0].getnewaddress(): 110000, self.nodes[1].getnewaddress(): 220000, self.nodes[0].getnewaddress(): 330000, self.nodes[1].getnewaddress(): 440000} txid = self.nodes[1].sendmany("", send_to) self.sync_all() assert_array_result(self.nodes[1].listtransactions(), {"category": "send", "amount": Decimal("-110000")}, {"txid": txid}) assert_array_result(self.nodes[0].listtransactions(), {"category": "receive", "amount": Decimal("110000")}, {"txid": txid}) assert_array_result(self.nodes[1].listtransactions(), {"category": "send", "amount": Decimal("-220000")}, {"txid": txid}) assert_array_result(self.nodes[1].listtransactions(), {"category": "receive", "amount": Decimal("220000")}, {"txid": txid}) assert_array_result(self.nodes[1].listtransactions(), {"category": "send", "amount": Decimal("-330000")}, {"txid": txid}) assert_array_result(self.nodes[0].listtransactions(), {"category": "receive", "amount": Decimal("330000")}, {"txid": txid}) assert_array_result(self.nodes[1].listtransactions(), {"category": "send", "amount": Decimal("-440000")}, {"txid": txid}) assert_array_result(self.nodes[1].listtransactions(), {"category": "receive", "amount": Decimal("440000")}, {"txid": txid}) 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"], 100000) self.generate(self.nodes[1], 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("100000")}, {"txid": txid, "label": "watchonly"}) if __name__ == '__main__': ListTransactionsTest().main() diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index 34e0d29ff..8433a50ac 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -1,124 +1,123 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test tx status in case of reorgs while wallet being shutdown. Wallet txn status rely on block connection/disconnection for its accuracy. In case of reorgs happening while wallet being shutdown block updates are not going to be received. At wallet loading, we check against chain if confirmed txn are still in chain and change their status if block in which they have been included has been disconnected. """ import os import shutil from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class ReorgsRestoreTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [["-noparkdeepreorg"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Send a tx from which to conflict outputs later txid_conflict_from = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) self.generate(self.nodes[0], 1) - self.sync_blocks() # Disconnect node1 from others to reorg its chain later 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("10000000")) tx = self.nodes[0].gettransaction(txid) self.generate(self.nodes[0], 4, sync_fun=self.no_op) tx_before_reorg = self.nodes[0].gettransaction(txid) assert_equal(tx_before_reorg["confirmations"], 4) # Disconnect node0 from node2 to broadcast a conflict on their # respective chains 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("10000000")) inputs = [] inputs.append({"txid": txid_conflict_from, "vout": nA}) outputs_1 = {} outputs_2 = {} # Create a conflicted tx broadcast on node0 chain and conflicting tx # broadcast on node1 chain. Both spend from txid_conflict_from outputs_1[self.nodes[0].getnewaddress()] = Decimal("9999980") outputs_2[self.nodes[0].getnewaddress()] = Decimal("9999980") conflicted = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs_1)) conflicting = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs_2)) conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"]) self.generate(self.nodes[0], 1, sync_fun=self.no_op) conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"]) self.generate(self.nodes[2], 9, sync_fun=self.no_op) # Reconnect node0 and node2 and check that conflicted_txid is # effectively conflicted 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) assert_equal(conflicted["confirmations"], -9) assert_equal(conflicted["walletconflicts"][0], conflicting["txid"]) # Node0 wallet is shutdown self.restart_node(0) # The block chain re-orgs and the tx is included in a different block self.generate(self.nodes[1], 9, sync_fun=self.no_op) self.nodes[1].sendrawtransaction(tx["hex"]) self.generate(self.nodes[1], 1, sync_fun=self.no_op) self.nodes[1].sendrawtransaction(conflicted["hex"]) self.generate(self.nodes[1], 1, sync_fun=self.no_op) # 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, 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 assert_equal(tx_after_reorg["confirmations"], 2) assert tx_before_reorg["blockhash"] != tx_after_reorg["blockhash"] conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid) # Check that conflicted tx is confirmed again with blockhash different # than previously conflicting tx assert_equal(conflicted_after_reorg["confirmations"], 1) assert conflicting["blockhash"] != conflicted_after_reorg["blockhash"] if __name__ == '__main__': ReorgsRestoreTest().main() diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index d42c74da9..0b8eab9fb 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -1,423 +1,421 @@ #!/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 the send RPC command.""" from decimal import Decimal 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, ) class WalletSendTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [ ["-whitelist=127.0.0.1", ], ["-whitelist=127.0.0.1", ], ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, add_to_wallet=None, psbt=None, inputs=None, add_inputs=None, change_address=None, change_position=None, include_watching=None, locktime=None, lock_unspents=None, subtract_fee_from_outputs=None, fee_rate=None, expect_error=None): assert (amount is None) != (data is None) from_balance_before = from_wallet.getbalance() if to_wallet is None: assert amount is None else: to_untrusted_pending_before = \ to_wallet.getbalances()["mine"]["untrusted_pending"] if amount: dest = to_wallet.getnewaddress() outputs = {dest: amount} else: outputs = {"data": data} # Construct options dictionary options = {} if add_to_wallet is not None: options["add_to_wallet"] = add_to_wallet else: add_to_wallet = ( False if psbt else from_wallet.getwalletinfo()["private_keys_enabled"] ) if psbt is not None: options["psbt"] = psbt if inputs is not None: options["inputs"] = inputs if add_inputs is not None: options["add_inputs"] = add_inputs if change_address is not None: options["change_address"] = change_address if change_position is not None: options["change_position"] = change_position if include_watching is not None: options["include_watching"] = include_watching if locktime is not None: options["locktime"] = locktime if lock_unspents is not None: options["lock_unspents"] = lock_unspents if subtract_fee_from_outputs is not None: options["subtract_fee_from_outputs"] = subtract_fee_from_outputs if fee_rate is not None: options["fee_rate"] = fee_rate if len(options.keys()) == 0: options = None if expect_error is None: res = from_wallet.send( outputs=outputs, options=options) else: try: assert_raises_rpc_error( expect_error[0], expect_error[1], from_wallet.send, outputs=outputs, options=options) except AssertionError: # Provide debug info if the test fails self.log.error("Unexpected successful result:") self.log.error(options) res = from_wallet.send( outputs=outputs, options=options) self.log.error(res) if "txid" in res and add_to_wallet: self.log.error("Transaction details:") try: tx = from_wallet.gettransaction(res["txid"]) self.log.error(tx) self.log.error( "testmempoolaccept (transaction may already be in mempool):") self.log.error( from_wallet.testmempoolaccept([tx["hex"]])) except JSONRPCException as exc: self.log.error(exc) raise return if locktime: return res if (from_wallet.getwalletinfo()["private_keys_enabled"] and not include_watching): assert_equal(res["complete"], True) assert "txid" in res else: assert_equal(res["complete"], False) assert "txid" not in res assert "psbt" in res if add_to_wallet and not include_watching: # Ensure transaction exists in the wallet: tx = from_wallet.gettransaction(res["txid"]) assert tx # Ensure transaction exists in the mempool: tx = from_wallet.getrawtransaction(res["txid"], True) assert tx if amount: if subtract_fee_from_outputs: assert_equal( from_balance_before - from_wallet.getbalance(), amount) else: assert_greater_than( from_balance_before - from_wallet.getbalance(), amount) else: assert next( (out for out in tx["vout"] if out["scriptPubKey"] ["asm"] == "OP_RETURN 35"), None) else: assert_equal(from_balance_before, from_wallet.getbalance()) if to_wallet: self.sync_mempools() if add_to_wallet: if not subtract_fee_from_outputs: assert_equal( to_wallet.getbalances()["mine"]["untrusted_pending"], to_untrusted_pending_before + Decimal( amount if amount else 0)) else: assert_equal( to_wallet.getbalances()["mine"]["untrusted_pending"], to_untrusted_pending_before) return res def run_test(self): self.log.info("Setup wallets...") # w0 is a wallet with coinbase rewards 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") # w2 contains the private keys for w3 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) w3 = self.nodes[1].get_wallet_rpc("w3") for _ in range(3): a2_receive = w2.getnewaddress() # doesn't actually use change derivation a2_change = w2.getrawchangeaddress() res = w3.importmulti([{ "desc": w2.getaddressinfo(a2_receive)["desc"], "timestamp": "now", "keypool": True, "watchonly": True }, { "desc": w2.getaddressinfo(a2_change)["desc"], "timestamp": "now", "keypool": True, "internal": True, "watchonly": True }]) assert_equal(res, [{"success": True}, {"success": True}]) # fund w3 w0.sendtoaddress(a2_receive, 10_000_000) self.generate(self.nodes[0], 1) - 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) w4 = self.nodes[1].get_wallet_rpc("w4") for _ in range(3): a2_receive = w2.getnewaddress() res = w4.importmulti([{ "desc": w2.getaddressinfo(a2_receive)["desc"], "timestamp": "now", "keypool": False, "watchonly": True }]) assert_equal(res, [{"success": True}]) # fund w4 w0.sendtoaddress(a2_receive, 10_000_000) self.generate(self.nodes[0], 1) - self.sync_blocks() self.log.info("Send to address...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000) self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=True) self.log.info("Don't broadcast...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False) assert res["hex"] self.log.info("Return PSBT...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, psbt=True) assert res["psbt"] self.log.info( "Create transaction that spends to address, but don't broadcast...") self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False) 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_000_000) res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] self.log.info( "Create PSBT from wallet w4 with watch-only keys, sign with w2...") self.test_send(from_wallet=w4, to_wallet=w1, amount=1_000_000, expect_error=(-4, "Insufficient funds")) res = self.test_send( from_wallet=w4, to_wallet=w1, amount=1_000_000, include_watching=True, add_to_wallet=False) res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] self.log.info("Create OP_RETURN...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000) self.test_send(from_wallet=w0, data="Hello World", expect_error=(-8, "Data must be hexadecimal string (not 'Hello World')")) self.test_send(from_wallet=w0, data="23") res = self.test_send(from_wallet=w3, data="23") 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_000_000, fee_rate=Decimal("20.00"), add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] assert_fee_amount(fee, len(res["hex"]) // 2, Decimal("20.00")) self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, fee_rate=-1, expect_error=(-3, "Amount out of range")) # Fee rate of 0.1 satoshi per byte should throw an error self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, fee_rate=Decimal("1.00"), expect_error=(-4, "Fee rate (1.00 XEC/kB) is lower than the minimum fee rate setting (10.00 XEC/kB)")) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, # feeRate=Decimal("1.00"), add_to_wallet=False) # assert res["hex"] # hex = res["hex"] # res = self.nodes[0].testmempoolaccept([hex]) # assert not res[0]["allowed"] # assert_equal(res[0]["reject-reason"], "...") # low fee # assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("1.00")) self.log.info( "If inputs are specified, do not automatically add more...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[], add_to_wallet=False) assert res["complete"] utxo1 = w0.listunspent()[0] assert_equal(utxo1["amount"], 50_000_000) self.test_send(from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], expect_error=(-4, "Insufficient funds")) self.test_send(from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], add_inputs=False, expect_error=(-4, "Insufficient funds")) res = self.test_send( from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], add_inputs=True, add_to_wallet=False) assert res["complete"] self.log.info("Manual change address and position...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, change_address="not an address", expect_error=(-5, "Change address must be a valid bitcoin address")) change_address = w0.getnewaddress() self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False, change_address=change_address) assert res["complete"] res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False, change_address=change_address, change_position=0) assert res["complete"] assert_equal( self.nodes[0].decodepsbt( res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["addresses"], [change_address]) self.log.info("Set lock time...") height = self.nodes[0].getblockchaininfo()["blocks"] res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, locktime=height + 1) assert res["complete"] assert res["txid"] txid = res["txid"] # Although the wallet finishes the transaction, it can't be added to # the mempool yet: hex = self.nodes[0].gettransaction(res["txid"])["hex"] res = self.nodes[0].testmempoolaccept([hex]) assert not res[0]["allowed"] assert_equal(res[0]["reject-reason"], "bad-txns-nonfinal") # It shouldn't be confirmed in the next block self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 0) # The mempool should allow it now: res = self.nodes[0].testmempoolaccept([hex]) assert res[0]["allowed"] # Don't wait for wallet to add it to the mempool: res = self.nodes[0].sendrawtransaction(hex) self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 1) self.log.info("Lock unspents...") utxo1 = w0.listunspent()[0] assert_greater_than(utxo1["amount"], 1_000_000) res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, inputs=[utxo1], add_to_wallet=False, lock_unspents=True) assert res["complete"] locked_coins = w0.listlockunspent() assert_equal(len(locked_coins), 1) # Locked coins are automatically unlocked when manually selected res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, inputs=[utxo1], add_to_wallet=False) assert res["complete"] self.log.info("Subtract fee from output") self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, subtract_fee_from_outputs=[0]) if __name__ == '__main__': WalletSendTest().main() diff --git a/test/functional/wallet_txn_clone.py b/test/functional/wallet_txn_clone.py index ccfe04189..f6140d124 100755 --- a/test/functional/wallet_txn_clone.py +++ b/test/functional/wallet_txn_clone.py @@ -1,146 +1,145 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet accounts properly when there are cloned transactions with malleated scriptsigs.""" import io from test_framework.messages import XEC, CTransaction from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class TxnMallTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], []] self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true", help="Test double-spend of 1-confirmed transaction") def setup_network(self): # Start with split network: super().setup_network() self.disconnect_nodes(1, 2) def run_test(self): output_type = "legacy" # All nodes should start with 1,250,000,000 XEC: starting_balance = 1250000000 for i in range(3): assert_equal(self.nodes[i].getbalance(), starting_balance) self.nodes[0].settxfee(1000) node0_address1 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid1 = self.nodes[0].sendtoaddress(node0_address1, 1219000000) node0_tx1 = self.nodes[0].gettransaction(node0_txid1) node0_address2 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid2 = self.nodes[0].sendtoaddress(node0_address2, 29000000) node0_tx2 = self.nodes[0].gettransaction(node0_txid2) assert_equal(self.nodes[0].getbalance(), starting_balance + node0_tx1["fee"] + node0_tx2["fee"]) # Coins are sent to node1_address node1_address = self.nodes[1].getnewaddress() # Send tx1, and another transaction tx2 that won't be cloned txid1 = self.nodes[0].sendtoaddress(node1_address, 40000000) txid2 = self.nodes[0].sendtoaddress(node1_address, 20000000) # Construct a clone of tx1, to be malleated rawtx1 = self.nodes[0].getrawtransaction(txid1, 1) clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"], "sequence": rawtx1["vin"][0]["sequence"]}] clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][0]["value"], rawtx1["vout"][1]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][1]["value"]} clone_locktime = rawtx1["locktime"] clone_raw = self.nodes[0].createrawtransaction( clone_inputs, clone_outputs, clone_locktime) # createrawtransaction randomizes the order of its outputs, so swap # them if necessary. clone_tx = CTransaction() clone_tx.deserialize(io.BytesIO(bytes.fromhex(clone_raw))) if (rawtx1["vout"][0]["value"] == 40000000 and clone_tx.vout[0].nValue != 40000000 * XEC or rawtx1["vout"][0]["value"] != 40000000 and clone_tx.vout[0].nValue == 40000000 * XEC): (clone_tx.vout[0], clone_tx.vout[1]) = (clone_tx.vout[1], clone_tx.vout[0]) # Use a different signature hash type to sign. This creates an equivalent but malleated clone. # Don't send the clone anywhere yet tx1_clone = self.nodes[0].signrawtransactionwithwallet( clone_tx.serialize().hex(), None, "ALL|FORKID|ANYONECANPAY") assert_equal(tx1_clone["complete"], True) # Have node0 mine a block, if requested: if (self.options.mine_block): self.generate( self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2])) tx1 = self.nodes[0].gettransaction(txid1) tx2 = self.nodes[0].gettransaction(txid2) # Node0's balance should be starting balance, plus 50BTC for another # matured block, minus tx1 and tx2 amounts, and minus transaction fees: expected = starting_balance + node0_tx1["fee"] + node0_tx2["fee"] if self.options.mine_block: expected += 50000000 expected += tx1["amount"] + tx1["fee"] expected += tx2["amount"] + tx2["fee"] assert_equal(self.nodes[0].getbalance(), expected) if self.options.mine_block: assert_equal(tx1["confirmations"], 1) assert_equal(tx2["confirmations"], 1) else: assert_equal(tx1["confirmations"], 0) assert_equal(tx2["confirmations"], 0) # Send clone and its parent to miner self.nodes[2].sendrawtransaction(node0_tx1["hex"]) txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"]) # ... mine a block... self.generate(self.nodes[2], 1, sync_fun=self.no_op) # Reconnect the split network, and sync chain: self.connect_nodes(1, 2) self.nodes[2].sendrawtransaction(node0_tx2["hex"]) self.nodes[2].sendrawtransaction(tx2["hex"]) # Mine another block to make sure we sync self.generate(self.nodes[2], 1) - self.sync_blocks() # Re-fetch transaction info: tx1 = self.nodes[0].gettransaction(txid1) tx1_clone = self.nodes[0].gettransaction(txid1_clone) tx2 = self.nodes[0].gettransaction(txid2) # Verify expected confirmations assert_equal(tx1["confirmations"], -2) assert_equal(tx1_clone["confirmations"], 2) assert_equal(tx2["confirmations"], 1) # Check node0's total balance; should be same as before the clone, + # 100,000,000 XEC for 2 matured, less possible orphaned matured subsidy expected += 100000000 if (self.options.mine_block): expected -= 50000000 assert_equal(self.nodes[0].getbalance(), expected) if __name__ == '__main__': TxnMallTest().main() diff --git a/test/functional/wallet_txn_doublespend.py b/test/functional/wallet_txn_doublespend.py index 8aeec17c4..0a3207ddb 100755 --- a/test/functional/wallet_txn_doublespend.py +++ b/test/functional/wallet_txn_doublespend.py @@ -1,149 +1,148 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet accounts properly when there is a double-spend conflict.""" from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, find_output class TxnMallTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], []] self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true", help="Test double-spend of 1-confirmed transaction") def setup_network(self): # Start with split network: super().setup_network() self.disconnect_nodes(1, 2) def run_test(self): # All nodes should start with 1,250,000,000 XEC: starting_balance = 1250000000 # All nodes should be out of IBD. # If the nodes are not all out of IBD, that can interfere with # blockchain sync later in the test when nodes are connected, due to # timing issues. for n in self.nodes: assert n.getblockchaininfo()["initialblockdownload"] is False for i in range(3): assert_equal(self.nodes[i].getbalance(), starting_balance) # Assign coins to foo and bar addresses: node0_address_foo = self.nodes[0].getnewaddress() fund_foo_txid = self.nodes[0].sendtoaddress( node0_address_foo, 1219000000) fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid) node0_address_bar = self.nodes[0].getnewaddress() fund_bar_txid = self.nodes[0].sendtoaddress( node0_address_bar, 29000000) fund_bar_tx = self.nodes[0].gettransaction(fund_bar_txid) assert_equal(self.nodes[0].getbalance(), starting_balance + fund_foo_tx["fee"] + fund_bar_tx["fee"]) # Coins are sent to node1_address node1_address = self.nodes[1].getnewaddress() # First: use raw transaction API to send 1,240,000,000 XEC to # node1_address, but don't broadcast: doublespend_fee = Decimal('-20000') rawtx_input_0 = {} rawtx_input_0["txid"] = fund_foo_txid rawtx_input_0["vout"] = find_output( self.nodes[0], fund_foo_txid, 1219000000) rawtx_input_1 = {} rawtx_input_1["txid"] = fund_bar_txid rawtx_input_1["vout"] = find_output( self.nodes[0], fund_bar_txid, 29000000) inputs = [rawtx_input_0, rawtx_input_1] change_address = self.nodes[0].getnewaddress() outputs = {} outputs[node1_address] = 1240000000 outputs[change_address] = 1248000000 - 1240000000 + doublespend_fee rawtx = self.nodes[0].createrawtransaction(inputs, outputs) doublespend = self.nodes[0].signrawtransactionwithwallet(rawtx) assert_equal(doublespend["complete"], True) # Create two spends using 1 50,000,000 XEC coin each txid1 = self.nodes[0].sendtoaddress(node1_address, 40000000) txid2 = self.nodes[0].sendtoaddress(node1_address, 20000000) # Have node0 mine a block: if (self.options.mine_block): self.generate( self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2])) tx1 = self.nodes[0].gettransaction(txid1) tx2 = self.nodes[0].gettransaction(txid2) # Node0's balance should be starting balance, plus 50,000,000 XEC for # another matured block, minus 40,000,000, minus 20,000,000, and minus # transaction fees: expected = starting_balance + fund_foo_tx["fee"] + fund_bar_tx["fee"] if self.options.mine_block: expected += 50000000 expected += tx1["amount"] + tx1["fee"] expected += tx2["amount"] + tx2["fee"] assert_equal(self.nodes[0].getbalance(), expected) if self.options.mine_block: assert_equal(tx1["confirmations"], 1) assert_equal(tx2["confirmations"], 1) # Node1's balance should be both transaction amounts: assert_equal(self.nodes[1].getbalance( ), starting_balance - tx1["amount"] - tx2["amount"]) else: assert_equal(tx1["confirmations"], 0) assert_equal(tx2["confirmations"], 0) # Now give doublespend and its parents to miner: self.nodes[2].sendrawtransaction(fund_foo_tx["hex"]) self.nodes[2].sendrawtransaction(fund_bar_tx["hex"]) doublespend_txid = self.nodes[2].sendrawtransaction(doublespend["hex"]) # ... mine a block... self.generate(self.nodes[2], 1, sync_fun=self.no_op) # Reconnect the split network, and sync chain: self.connect_nodes(1, 2) # Mine another block to make sure we sync self.generate(self.nodes[2], 1) - self.sync_blocks() assert_equal(self.nodes[0].gettransaction( doublespend_txid)["confirmations"], 2) # Re-fetch transaction info: tx1 = self.nodes[0].gettransaction(txid1) tx2 = self.nodes[0].gettransaction(txid2) # Both transactions should be conflicted assert_equal(tx1["confirmations"], -2) assert_equal(tx2["confirmations"], -2) # Node0's total balance should be starting balance, plus 100BTC for # two more matured blocks, minus 1240 for the double-spend, plus fees (which are # negative): expected = starting_balance + 100000000 - 1240000000 + \ fund_foo_tx["fee"] + fund_bar_tx["fee"] + doublespend_fee assert_equal(self.nodes[0].getbalance(), expected) # Node1's balance should be its initial balance (1250 for 25 block # rewards) plus the doublespend: assert_equal(self.nodes[1].getbalance(), 1250000000 + 1240000000) if __name__ == '__main__': TxnMallTest().main()