diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index 262dbb3fc..28d6c3b7c 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -1,292 +1,292 @@ #!/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 BIP 37 """ from test_framework.messages import ( COIN, MAX_BLOOM_FILTER_SIZE, MAX_BLOOM_HASH_FUNCS, MSG_BLOCK, MSG_FILTERED_BLOCK, CInv, msg_filteradd, msg_filterclear, msg_filterload, msg_getdata, msg_mempool, msg_version, ) from test_framework.p2p import ( P2P_SERVICES, P2P_SUBVERSION, P2P_VERSION, P2PInterface, p2p_lock, ) from test_framework.script import MAX_SCRIPT_ELEMENT_SIZE from test_framework.test_framework import BitcoinTestFramework -from test_framework.wallet import MiniWallet, random_p2pkh +from test_framework.wallet import MiniWallet, getnewdestination class P2PBloomFilter(P2PInterface): # This is a P2SH watch-only wallet watch_script_pubkey = bytes.fromhex( 'a914ffffffffffffffffffffffffffffffffffffffff87') # The initial filter (n=10, fp=0.000001) with just the above scriptPubKey # added watch_filter_init = msg_filterload( data=b'@\x00\x08\x00\x80\x00\x00 \x00\xc0\x00 \x04\x00\x08$\x00\x04\x80\x00\x00 \x00\x00\x00\x00\x80\x00\x00@\x00\x02@ \x00', nHashFuncs=19, nTweak=0, nFlags=1, ) def __init__(self): super().__init__() self._tx_received = False self._merkleblock_received = False def on_inv(self, message): want = msg_getdata() for i in message.inv: # inv messages can only contain TX or BLOCK, so translate BLOCK to # FILTERED_BLOCK if i.type == MSG_BLOCK: want.inv.append(CInv(MSG_FILTERED_BLOCK, i.hash)) else: want.inv.append(i) if len(want.inv): self.send_message(want) def on_merkleblock(self, message): self._merkleblock_received = True def on_tx(self, message): self._tx_received = True @property def tx_received(self): with p2p_lock: return self._tx_received @tx_received.setter def tx_received(self, value): with p2p_lock: self._tx_received = value @property def merkleblock_received(self): with p2p_lock: return self._merkleblock_received @merkleblock_received.setter def merkleblock_received(self, value): with p2p_lock: self._merkleblock_received = value class FilterTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ '-peerbloomfilters', '-whitelist=noban@127.0.0.1', # immediate tx relay ]] def generatetoscriptpubkey(self, scriptpubkey): """Helper to generate a single block to the given scriptPubKey.""" return self.generatetodescriptor( self.nodes[0], 1, f'raw({scriptpubkey.hex()})')[0] def test_size_limits(self, filter_peer): self.log.info('Check that too large filter is rejected') with self.nodes[0].assert_debug_log(['Misbehaving']): filter_peer.send_and_ping(msg_filterload( data=b'\xbb' * (MAX_BLOOM_FILTER_SIZE + 1))) self.log.info('Check that max size filter is accepted') with self.nodes[0].assert_debug_log([""], unexpected_msgs=['Misbehaving']): filter_peer.send_and_ping( msg_filterload( data=b'\xbb' * (MAX_BLOOM_FILTER_SIZE))) filter_peer.send_and_ping(msg_filterclear()) self.log.info( 'Check that filter with too many hash functions is rejected') with self.nodes[0].assert_debug_log(['Misbehaving']): filter_peer.send_and_ping( msg_filterload( data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS + 1)) self.log.info('Check that filter with max hash functions is accepted') with self.nodes[0].assert_debug_log([""], unexpected_msgs=['Misbehaving']): filter_peer.send_and_ping( msg_filterload( data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS)) # Don't send filterclear until next two filteradd checks are done self.log.info( 'Check that max size data element to add to the filter is accepted') with self.nodes[0].assert_debug_log([""], unexpected_msgs=['Misbehaving']): filter_peer.send_and_ping( msg_filteradd( data=b'\xcc' * (MAX_SCRIPT_ELEMENT_SIZE))) self.log.info( 'Check that too large data element to add to the filter is rejected') with self.nodes[0].assert_debug_log(['Misbehaving']): filter_peer.send_and_ping(msg_filteradd( data=b'\xcc' * (MAX_SCRIPT_ELEMENT_SIZE + 1))) filter_peer.send_and_ping(msg_filterclear()) def test_msg_mempool(self): self.log.info( "Check that a node with bloom filters enabled services p2p mempool messages") filter_peer = P2PBloomFilter() self.log.debug("Create a tx relevant to the peer before connecting") txid, _ = self.wallet.send_to( from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) self.log.debug( "Send a mempool msg after connecting and check that the tx is received") self.nodes[0].add_p2p_connection(filter_peer) filter_peer.send_and_ping(filter_peer.watch_filter_init) filter_peer.send_message(msg_mempool()) filter_peer.wait_for_tx(txid) def test_frelay_false(self, filter_peer): self.log.info( "Check that a node with fRelay set to false does not receive invs until the filter is set") filter_peer.tx_received = False self.wallet.send_to( from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) # Sync to make sure the reason filter_peer doesn't receive the tx is # not p2p delays filter_peer.sync_with_ping() assert not filter_peer.tx_received # Clear the mempool so that this transaction does not impact subsequent # tests self.generate(self.nodes[0], 1) def test_filter(self, filter_peer): # Set the bloomfilter using filterload filter_peer.send_and_ping(filter_peer.watch_filter_init) # If fRelay is not already True, sending filterload sets it to True assert self.nodes[0].getpeerinfo()[0]['relaytxes'] self.log.info( 'Check that we receive merkleblock and tx if the filter matches a tx in a block') block_hash = self.generatetoscriptpubkey( filter_peer.watch_script_pubkey) txid = self.nodes[0].getblock(block_hash)['tx'][0] filter_peer.wait_for_merkleblock(block_hash) filter_peer.wait_for_tx(txid) self.log.info( 'Check that we only receive a merkleblock if the filter does not match a tx in a block') filter_peer.tx_received = False - block_hash = self.generatetoscriptpubkey(random_p2pkh()) + block_hash = self.generatetoscriptpubkey(getnewdestination()[1]) filter_peer.wait_for_merkleblock(block_hash) assert not filter_peer.tx_received self.log.info( 'Check that we not receive a tx if the filter does not match a mempool tx') filter_peer.merkleblock_received = False filter_peer.tx_received = False self.wallet.send_to( from_node=self.nodes[0], - scriptPubKey=random_p2pkh(), + scriptPubKey=getnewdestination()[1], amount=7 * COIN) filter_peer.sync_send_with_ping() assert not filter_peer.merkleblock_received assert not filter_peer.tx_received self.log.info( 'Check that we receive a tx if the filter matches a mempool tx') filter_peer.merkleblock_received = False txid, _ = self.wallet.send_to( from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) filter_peer.wait_for_tx(txid) assert not filter_peer.merkleblock_received self.log.info( 'Check that after deleting filter all txs get relayed again') filter_peer.send_and_ping(msg_filterclear()) for _ in range(5): txid, _ = self.wallet.send_to( - from_node=self.nodes[0], scriptPubKey=random_p2pkh(), amount=7 * COIN) + from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=7 * COIN) filter_peer.wait_for_tx(txid) self.log.info( 'Check that request for filtered blocks is ignored if no filter' ' is set') filter_peer.merkleblock_received = False filter_peer.tx_received = False with self.nodes[0].assert_debug_log(expected_msgs=['received getdata']): - block_hash = self.generatetoscriptpubkey(random_p2pkh()) + block_hash = self.generatetoscriptpubkey(getnewdestination()[1]) filter_peer.wait_for_inv([CInv(MSG_BLOCK, int(block_hash, 16))]) filter_peer.sync_with_ping() assert not filter_peer.merkleblock_received assert not filter_peer.tx_received self.log.info( 'Check that sending "filteradd" if no filter is set is treated as ' 'misbehavior') with self.nodes[0].assert_debug_log(['Misbehaving']): filter_peer.send_and_ping(msg_filteradd(data=b'letsmisbehave')) self.log.info( "Check that division-by-zero remote crash bug [CVE-2013-5700] is fixed") filter_peer.send_and_ping(msg_filterload(data=b'', nHashFuncs=1)) filter_peer.send_and_ping( msg_filteradd( data=b'letstrytocrashthisnode')) self.nodes[0].disconnect_p2ps() def run_test(self): self.wallet = MiniWallet(self.nodes[0]) self.wallet.rescan_utxos() filter_peer = self.nodes[0].add_p2p_connection(P2PBloomFilter()) self.log.info('Test filter size limits') self.test_size_limits(filter_peer) self.log.info('Test BIP 37 for a node with fRelay = True (default)') self.test_filter(filter_peer) self.nodes[0].disconnect_p2ps() self.log.info('Test BIP 37 for a node with fRelay = False') # Add peer but do not send version yet filter_peer_without_nrelay = self.nodes[0].add_p2p_connection( P2PBloomFilter(), send_version=False, wait_for_verack=False) # Send version with relay=False version_without_fRelay = msg_version() version_without_fRelay.nVersion = P2P_VERSION version_without_fRelay.strSubVer = P2P_SUBVERSION version_without_fRelay.nServices = P2P_SERVICES version_without_fRelay.relay = 0 filter_peer_without_nrelay.send_message(version_without_fRelay) filter_peer_without_nrelay.wait_for_verack() assert not self.nodes[0].getpeerinfo()[0]['relaytxes'] self.test_frelay_false(filter_peer_without_nrelay) self.test_filter(filter_peer_without_nrelay) self.test_msg_mempool() if __name__ == '__main__': FilterTest().main() diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 6e9f5b84a..565bc3637 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -1,231 +1,233 @@ #!/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 scantxoutset rpc call.""" -import os -import shutil from decimal import Decimal +from test_framework.messages import XEC from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.wallet import ( + MiniWallet, + address_to_scriptpubkey, + getnewdestination, +) def descriptors(out): return sorted(u['desc'] for u in out['unspents']) class ScantxoutsetTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.setup_clean_chain = True - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def sendtodestination(self, destination, amount): + # interpret strings as addresses, assume scriptPubKey otherwise + if isinstance(destination, str): + destination = address_to_scriptpubkey(destination) + self.wallet.send_to( + from_node=self.nodes[0], + scriptPubKey=destination, + amount=int( + XEC * amount)) def run_test(self): - self.log.info("Mining blocks...") - self.generate(self.nodes[0], 110) + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() - addr = self.nodes[0].getnewaddress("") - pubkey = self.nodes[0].getaddressinfo(addr)['pubkey'] - self.nodes[0].sendtoaddress(addr, 2000) + self.log.info("Create UTXOs...") + pubk, spk, addr = getnewdestination() + self.sendtodestination(spk, 2000) # send to child keys of tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK # (m/0'/0'/0') - self.nodes[0].sendtoaddress( + self.sendtodestination( "mkHV1C6JLheLoUSSZYk7x3FH5tnx9bu7yc", 8000) # (m/0'/0'/1') - self.nodes[0].sendtoaddress( + self.sendtodestination( "mipUSRmJAj2KrjSvsPQtnP8ynUon7FhpCR", 16000) # (m/0'/0'/1500') - self.nodes[0].sendtoaddress( + self.sendtodestination( "n37dAGe6Mq1HGM9t4b6rFEEsDGq7Fcgfqg", 32000) # (m/0'/0'/0) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mqS9Rpg8nNLAzxFExsgFLCnzHBsoQ3PRM6", 64000) # (m/0'/0'/1) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mnTg5gVWr3rbhHaKjJv7EEEc76ZqHgSj4S", 128000) # (m/0'/0'/1500) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mketCd6B9U9Uee1iCsppDJJBHfvi6U6ukC", 256000) # (m/1/1/0') - self.nodes[0].sendtoaddress( + self.sendtodestination( "mj8zFzrbBcdaWXowCQ1oPZ4qioBVzLzAp7", 512000) # (m/1/1/1') - self.nodes[0].sendtoaddress( + self.sendtodestination( "mfnKpKQEftniaoE1iXuMMePQU3PUpcNisA", 1024000) # (m/1/1/1500') - self.nodes[0].sendtoaddress( + self.sendtodestination( "mou6cB1kaP1nNJM1sryW6YRwnd4shTbXYQ", 2048000) # (m/1/1/0) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", 4096000) # (m/1/1/1) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mxp7w7j8S1Aq6L8StS2PqVvtt4HGxXEvdy", 8192000) # (m/1/1/1500) - self.nodes[0].sendtoaddress( + self.sendtodestination( "mpQ8rokAhp1TAtJQR6F6TaUmjAWkAWYYBq", 16384000) self.generate(self.nodes[0], 1) - self.log.info("Stop node, remove wallet, mine again some blocks...") - self.stop_node(0) - shutil.rmtree(os.path.join( - self.nodes[0].datadir, self.chain, 'wallets')) - self.start_node(0) - self.generate(self.nodes[0], 110) - scan = self.nodes[0].scantxoutset("start", []) info = self.nodes[0].gettxoutsetinfo() assert_equal(scan['success'], True) assert_equal(scan['height'], info['height']) assert_equal(scan['txouts'], info['txouts']) assert_equal(scan['bestblock'], info['bestblock']) - self.restart_node(0, ['-nowallet']) self.log.info("Test if we have found the non HD unspent outputs.") assert_equal(self.nodes[0].scantxoutset( - "start", ["pkh(" + pubkey + ")"])['total_amount'], Decimal("2000")) + "start", ["pkh(" + pubk.hex() + ")"])['total_amount'], Decimal("2000")) assert_equal(self.nodes[0].scantxoutset( - "start", ["combo(" + pubkey + ")"])['total_amount'], Decimal("2000")) + "start", ["combo(" + pubk.hex() + ")"])['total_amount'], Decimal("2000")) assert_equal(self.nodes[0].scantxoutset( "start", ["addr(" + addr + ")"])['total_amount'], Decimal("2000")) assert_equal(self.nodes[0].scantxoutset( "start", ["addr(" + addr + ")"])['total_amount'], Decimal("2000")) self.log.info("Test range validation.") assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": -1}]) assert_raises_rpc_error(-8, "Range should be greater or equal than 0", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [-1, 10]}]) assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]}]) assert_raises_rpc_error(-8, "Range specified as [begin,end] must not have begin after end", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [2, 1]}]) assert_raises_rpc_error(-8, "Range is too large", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [0, 1000001]}]) self.log.info("Test extended key derivation.") # Run various scans, and verify that the sum of the amounts of the matches corresponds to the expected subset. # Note that all amounts in the UTXO set are powers of 2 multiplied by # 0.001 BTC, so each amounts uniquely identifies a subset. assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/0h)"])['total_amount'], Decimal("8000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/1h)"])['total_amount'], Decimal("16000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500')"])['total_amount'], Decimal("32000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0h/0)"])['total_amount'], Decimal("64000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/1)"])['total_amount'], Decimal("128000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500)"])['total_amount'], Decimal("256000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*h)", "range": 1499}])['total_amount'], Decimal("24000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/*h)", "range": 1500}])['total_amount'], Decimal("56000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])['total_amount'], Decimal("192000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*)", "range": 1500}])['total_amount'], Decimal("448000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0')"])['total_amount'], Decimal("0512000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1')"])['total_amount'], Decimal("1024000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500h)"])['total_amount'], Decimal("2048000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])['total_amount'], Decimal("4096000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1)"])['total_amount'], Decimal("8192000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500)"])['total_amount'], Decimal("16384000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0)"])['total_amount'], Decimal("4096000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo([abcdef88/1/2'/3/4h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1)"])['total_amount'], Decimal("8192000")) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1500)"])['total_amount'], Decimal("16384000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1499}])['total_amount'], Decimal("1536000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1500}])['total_amount'], Decimal("3584000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1499}])['total_amount'], Decimal("12288000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1500}])['total_amount'], Decimal("28672000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12288000")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28672000")) assert_equal( self.nodes[0].scantxoutset( "start", [ { "desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": [ 1500, 1500]}])['total_amount'], Decimal("16384000")) # Test the reported descriptors for a few matches assert_equal(descriptors(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])), ["pkh([0c5f9a1e/0'/0'/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)#dzxw429x", "pkh([0c5f9a1e/0'/0'/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)#43rvceed"]) assert_equal( descriptors( self.nodes[0].scantxoutset( "start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8"]) assert_equal(descriptors(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])), ['pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8', 'pkh([0c5f9a1e/1/1/1500]03832901c250025da2aebae2bfb38d5c703a57ab66ad477f9c578bfbcd78abca6f)#vchwd07g', 'pkh([0c5f9a1e/1/1/1]030d820fc9e8211c4169be8530efbc632775d8286167afd178caaf1089b77daba7)#z2t3ypsa']) # Check that status and abort don't need second arg assert_equal(self.nodes[0].scantxoutset("status"), None) assert_equal(self.nodes[0].scantxoutset("abort"), False) # Check that second arg is needed for start assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", self.nodes[0].scantxoutset, "start") if __name__ == '__main__': ScantxoutsetTest().main() diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 7b57d0598..fdc3be65b 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -1,248 +1,268 @@ #!/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. """A limited-functionality wallet, which may replace a real wallet in tests""" from copy import deepcopy from decimal import Decimal from typing import Optional from test_framework.address import ( ADDRESS_ECREG_P2SH_OP_TRUE, SCRIPTSIG_OP_TRUE, + base58_to_byte, + key_to_p2pkh, ) from test_framework.key import ECKey from test_framework.messages import ( XEC, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, ToHex, ) from test_framework.script import ( OP_CHECKSIG, OP_DUP, + OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, CScript, hash160, ) from test_framework.txtools import pad_tx from test_framework.util import ( assert_equal, assert_greater_than_or_equal, satoshi_round, ) DEFAULT_FEE = Decimal("100.00") class MiniWallet: def __init__(self, test_node): self._test_node = test_node self._utxos = [] self._address = ADDRESS_ECREG_P2SH_OP_TRUE self._scriptPubKey = bytes.fromhex( self._test_node.validateaddress( self._address)['scriptPubKey']) def rescan_utxos(self): """Drop all utxos and rescan the utxo set""" self._utxos = [] res = self._test_node.scantxoutset( action="start", scanobjects=[f'raw({self._scriptPubKey.hex()})']) assert_equal(True, res['success']) for utxo in res['unspents']: self._utxos.append( {'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount'], 'height': utxo['height']}) def scan_tx(self, tx): """Scan the tx for self._scriptPubKey outputs and add them to self._utxos""" for out in tx['vout']: if out['scriptPubKey']['hex'] == self._scriptPubKey.hex(): self._utxos.append( {'txid': tx['txid'], 'vout': out['n'], 'value': out['value'], 'height': 0}) def generate(self, num_blocks, **kwargs): """Generate blocks with coinbase outputs to the internal address, and append the outputs to the internal list""" blocks = self._test_node.generatetodescriptor( num_blocks, f'raw({self._scriptPubKey.hex()})', **kwargs) for b in blocks: block_info = self._test_node.getblock(blockhash=b, verbosity=2) cb_tx = block_info['tx'][0] self._utxos.append( {'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value'], 'height': block_info['height']}) return blocks def get_utxo(self, *, txid: Optional[str] = ''): """ Returns a utxo and marks it as spent (pops it from the internal list) Args: txid: get the first utxo we find from a specific transaction """ # by default the last utxo index = -1 # Put the largest utxo last self._utxos = sorted( self._utxos, key=lambda k: ( k['value'], -k['height'])) if txid: utxo = next(filter(lambda utxo: txid == utxo['txid'], self._utxos)) index = self._utxos.index(utxo) return self._utxos.pop(index) def send_self_transfer(self, **kwargs): """Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" tx = self.create_self_transfer(**kwargs) self.sendrawtransaction( from_node=kwargs['from_node'], tx_hex=tx['hex']) return tx def send_to(self, *, from_node, scriptPubKey, amount, fee=1000): """ Create and send a tx with an output to a given scriptPubKey/amount, plus a change output to our internal address. To keep things simple, a fixed fee given in Satoshi is used. Note that this method fails if there is no single internal utxo available that can cover the cost for the amount and the fixed fee (the utxo with the largest value is taken). Returns a tuple (txid, n) referring to the created external utxo outpoint. """ tx = self.create_self_transfer(from_node=from_node, fee_rate=0)["tx"] assert_greater_than_or_equal(tx.vout[0].nValue, amount + fee) # change output -> MiniWallet tx.vout[0].nValue -= (amount + fee) # arbitrary output -> to be returned tx.vout.append(CTxOut(amount, scriptPubKey)) txid = self.sendrawtransaction( from_node=from_node, tx_hex=tx.serialize().hex()) return txid, len(tx.vout) - 1 def create_self_transfer(self, *, fee_rate=Decimal("3000.00"), from_node, utxo_to_spend=None, locktime=0): """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" utxo_to_spend = utxo_to_spend or self.get_utxo() # The size will be enforced by pad_tx() size = 100 send_value = satoshi_round( utxo_to_spend['value'] - fee_rate * (Decimal(size) / 1000)) assert send_value > 0 tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']))] tx.vout = [CTxOut(int(send_value * XEC), self._scriptPubKey)] tx.nLockTime = locktime tx.vin[0].scriptSig = SCRIPTSIG_OP_TRUE pad_tx(tx, size) tx_hex = tx.serialize().hex() assert_equal(len(tx.serialize()), size) return {'txid': tx.rehash(), 'hex': tx_hex, 'tx': tx} def sendrawtransaction(self, *, from_node, tx_hex): txid = from_node.sendrawtransaction(tx_hex) self.scan_tx(from_node.decoderawtransaction(tx_hex)) return txid -def random_p2pkh(): - """Generate a random P2PKH scriptPubKey. Can be used when a random destination is needed, - but no compiled wallet is available (e.g. as replacement to the getnewaddress RPC).""" +def getnewdestination(): + """Generate a random destination and return the corresponding public key, + scriptPubKey and address. Can be used when a random destination is + needed, but no compiled wallet is available (e.g. as replacement to the + getnewaddress/getaddressinfo RPCs).""" key = ECKey() key.generate() - return CScript([OP_DUP, OP_HASH160, hash160( - key.get_pubkey().get_bytes()), OP_EQUALVERIFY, OP_CHECKSIG]) + pubkey = key.get_pubkey().get_bytes() + scriptpubkey = CScript( + [OP_DUP, OP_HASH160, hash160(pubkey), OP_EQUALVERIFY, OP_CHECKSIG]) + return pubkey, scriptpubkey, key_to_p2pkh(pubkey) + + +def address_to_scriptpubkey(address): + """Converts a given address to the corresponding output script (scriptPubKey).""" + payload, version = base58_to_byte(address) + if version == 111: # testnet pubkey hash + return CScript([OP_DUP, OP_HASH160, payload, + OP_EQUALVERIFY, OP_CHECKSIG]) + elif version == 196: # testnet script hash + return CScript([OP_HASH160, payload, OP_EQUAL]) + # TODO: also support other address formats + else: + assert False def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE): """Build a transaction that spends parent_txid.vout[n] and produces one output with amount = parent_value with a fee deducted. Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created). """ inputs = [{"txid": parent_txid, "vout": n}] my_value = parent_value - fee outputs = {address: my_value} rawtx = node.createrawtransaction(inputs, outputs) prevtxs = [{ "txid": parent_txid, "vout": n, "scriptPubKey": parent_locking_script, "amount": parent_value, }] if parent_locking_script else None signedtx = node.signrawtransactionwithkey( hexstring=rawtx, privkeys=privkeys, prevtxs=prevtxs) assert signedtx["complete"] tx = FromHex(CTransaction(), signedtx["hex"]) return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex()) def create_child_with_parents(node, address, privkeys, parents_tx, values, locking_scripts, fee=DEFAULT_FEE): """Creates a transaction that spends the first output of each parent in parents_tx.""" num_parents = len(parents_tx) total_value = sum(values) inputs = [{"txid": tx.get_id(), "vout": 0} for tx in parents_tx] outputs = {address: total_value - fee} rawtx_child = node.createrawtransaction(inputs, outputs) prevtxs = [] for i in range(num_parents): prevtxs.append( {"txid": parents_tx[i].get_id(), "vout": 0, "scriptPubKey": locking_scripts[i], "amount": values[i]}) signedtx_child = node.signrawtransactionwithkey( hexstring=rawtx_child, privkeys=privkeys, prevtxs=prevtxs) assert signedtx_child["complete"] return signedtx_child["hex"] def create_raw_chain(node, first_coin, address, privkeys, chain_length=50): """Helper function: create a "chain" of chain_length transactions. The nth transaction in the chain is a child of the n-1th transaction and parent of the n+1th transaction. """ parent_locking_script = None txid = first_coin["txid"] chain_hex = [] chain_txns = [] value = first_coin["amount"] for _ in range(chain_length): (tx, txhex, value, parent_locking_script) = make_chain( node, address, privkeys, txid, value, 0, parent_locking_script) txid = tx.get_id() chain_hex.append(txhex) chain_txns.append(tx) return (chain_hex, chain_txns) def bulk_transaction( tx: CTransaction, node, target_size: int, privkeys=None, prevtxs=None ) -> CTransaction: """Return a padded and signed transaction. The original transaction is left unaltered. If privkeys is not specified, it is assumed that the transaction has an anyone-can-spend output as unique output. """ tx_heavy = deepcopy(tx) pad_tx(tx_heavy, target_size) assert_greater_than_or_equal(tx_heavy.billable_size(), target_size) if privkeys is not None: signed_tx = node.signrawtransactionwithkey( ToHex(tx_heavy), privkeys, prevtxs) return FromHex(CTransaction(), signed_tx["hex"]) # OP_TRUE tx_heavy.vin[0].scriptSig = SCRIPTSIG_OP_TRUE return tx_heavy