def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): # Make sure that this folder never ever gets a reasonable non-testing use-case data_folder = "./test_specter_data_3456778" shutil.rmtree(data_folder, ignore_errors=True) config = { "rpc": { "autodetect": False, "user": bitcoin_regtest.rpcconn.rpcuser, "password": bitcoin_regtest.rpcconn.rpcpassword, "port": bitcoin_regtest.rpcconn.rpcport, "host": bitcoin_regtest.rpcconn.ipaddress, "protocol": "http", }, "auth": "rpcpasswordaspin", } specter = Specter(data_folder=devices_filled_data_folder, config=config) specter.check() yield specter shutil.rmtree(data_folder, ignore_errors=True)
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): # Make sure that this folder never ever gets a reasonable non-testing use-case config = { "rpc": { "autodetect": False, "datadir": "", "user": bitcoin_regtest.rpcconn.rpcuser, "password": bitcoin_regtest.rpcconn.rpcpassword, "port": bitcoin_regtest.rpcconn.rpcport, "host": bitcoin_regtest.rpcconn.ipaddress, "protocol": "http", }, "auth": { "method": "rpcpasswordaspin", }, } specter = Specter(data_folder=devices_filled_data_folder, config=config) specter.check() assert not specter.wallet_manager.working_folder is None yield specter
def test_abandon_purged_tx(caplog, docker, request, devices_filled_data_folder, device_manager): # Specter should support calling abandontransaction if a pending tx has been purged # from the mempool. Test starts a new bitcoind with a restricted mempool to make it # easier to spam the mempool and purge our target tx. # TODO: Similar test but for maxmempoolexpiry? # Copied and adapted from: # https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_limit.py from bitcoin_core.test.functional.test_framework.util import ( gen_return_txouts, satoshi_round, create_lots_of_big_transactions, ) from conftest import instantiate_bitcoind_controller caplog.set_level(logging.DEBUG) # ==== Specter-specific: do custom setup ==== # Instantiate a new bitcoind w/limited mempool. Use a different port to not interfere # with existing instance for other tests. bitcoind_controller = instantiate_bitcoind_controller( docker, request, rpcport=18998, extra_args=[ "-acceptnonstdtxn=1", "-maxmempool=5", "-spendzeroconfchange=0" ], ) rpcconn = bitcoind_controller.rpcconn rpc = rpcconn.get_rpc() assert rpc is not None assert rpc.ipaddress != None # Note: Our utxo creation is simpler than mempool_limit.py's approach since we're # running in regtest and can just use generatetoaddress(). # Instantiate a new Specter instance to talk to this bitcoind config = { "rpc": { "autodetect": False, "user": rpcconn.rpcuser, "password": rpcconn.rpcpassword, "port": rpcconn.rpcport, "host": rpcconn.ipaddress, "protocol": "http", }, "auth": { "method": "rpcpasswordaspin", }, } specter = Specter(data_folder=devices_filled_data_folder, config=config) specter.check() specter.check_node_info() assert specter._info["mempool_info"][ "maxmempool"] == 5 * 1000 * 1000 # 5MB # Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt. # TODO: Make a test fixture in conftest.py that sets up already funded wallets # for a bitcoin core hot wallet. wallet_manager = WalletManager( 200100, devices_filled_data_folder, rpc, "regtest", device_manager, ) # Create a new device that can sign psbts (Bitcoin Core hot wallet) device = device_manager.add_device(name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]) device.setup_device(file_password=None, wallet_manager=wallet_manager) device.add_hot_wallet_keys( mnemonic=generate_mnemonic(strength=128), passphrase="", paths=["m/49h/0h/0h"], file_password=None, wallet_manager=wallet_manager, testnet=True, keys_range=[0, 1000], keys_purposes=[], ) wallet = wallet_manager.create_wallet("bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]) # Fund the wallet. Going to need a LOT of utxos to play with. logging.info("Generating utxos to wallet") address = wallet.getnewaddress() wallet.rpc.generatetoaddress(91, address) # newly minted coins need 100 blocks to get spendable # let's mine another 100 blocks to get these coins spendable wallet.rpc.generatetoaddress(101, address) # update the wallet data wallet.get_balance() # ==== Begin test from mempool_limit.py ==== txouts = gen_return_txouts() relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) logging.info("Check that mempoolminfee is minrelytxfee") assert satoshi_round( rpc.getmempoolinfo()["minrelaytxfee"]) == Decimal("0.00001000") assert satoshi_round( rpc.getmempoolinfo()["mempoolminfee"]) == Decimal("0.00001000") txids = [] utxos = wallet.rpc.listunspent() logging.info("Create a mempool tx that will be evicted") us0 = utxos.pop() inputs = [{"txid": us0["txid"], "vout": us0["vout"]}] outputs = {wallet.getnewaddress(): 0.0001} tx = wallet.rpc.createrawtransaction(inputs, outputs) wallet.rpc.settxfee( str(relayfee)) # specifically fund this tx with low fee txF = wallet.rpc.fundrawtransaction(tx) wallet.rpc.settxfee(0) # return to automatic fee selection txFS = device.sign_raw_tx(txF["hex"], wallet) txid = wallet.rpc.sendrawtransaction(txFS["hex"]) # ==== Specter-specific: can't abandon a valid pending tx ==== try: wallet.abandontransaction(txid) except SpecterError as e: assert "Cannot abandon" in str(e) # ==== Resume test from mempool_limit.py ==== # Spam the mempool with big transactions! relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) base_fee = float(relayfee) * 100 for i in range(3): txids.append([]) txids[i] = create_lots_of_big_transactions(wallet, txouts, utxos[30 * i:30 * i + 30], 30, (i + 1) * base_fee) logging.info("The tx should be evicted by now") assert txid not in wallet.rpc.getrawmempool() txdata = wallet.rpc.gettransaction(txid) assert txdata["confirmations"] == 0 # confirmation should still be 0 # ==== Specter-specific: Verify purge and abandon ==== assert wallet.is_tx_purged(txid) wallet.abandontransaction(txid) # tx will still be in the wallet but marked "abandoned" txdata = wallet.rpc.gettransaction(txid) for detail in txdata["details"]: if detail["category"] == "send": assert detail["abandoned"] # Can we now spend those same inputs? outputs = {wallet.getnewaddress(): 0.0001} tx = wallet.rpc.createrawtransaction(inputs, outputs) # Fund this tx with a high enough fee relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) wallet.rpc.settxfee(str(relayfee * Decimal("3.0"))) txF = wallet.rpc.fundrawtransaction(tx) wallet.rpc.settxfee(0) # return to automatic fee selection txFS = device.sign_raw_tx(txF["hex"], wallet) txid = wallet.rpc.sendrawtransaction(txFS["hex"]) # Should have been accepted by the mempool assert txid in wallet.rpc.getrawmempool() assert wallet.get_balance()["untrusted_pending"] == 0.0001 # Clean up bitcoind_controller.stop_bitcoind()
def test_import_address_labels(caplog, docker, request, devices_filled_data_folder, device_manager): caplog.set_level(logging.DEBUG) # ==== Specter-specific: do custom setup ==== # Instantiate a new bitcoind w/limited mempool. Use a different port to not interfere # with existing instance for other tests. bitcoind_controller = instantiate_bitcoind_controller( docker, request, rpcport=18968, extra_args=[ "-acceptnonstdtxn=1", "-maxmempool=5", "-spendzeroconfchange=0" ], ) try: assert bitcoind_controller.get_rpc().test_connection() rpcconn = bitcoind_controller.rpcconn rpc = rpcconn.get_rpc() assert rpc is not None assert rpc.ipaddress != None # Note: Our utxo creation is simpler than mempool_limit.py's approach since we're # running in regtest and can just use generatetoaddress(). # Instantiate a new Specter instance to talk to this bitcoind config = { "rpc": { "autodetect": False, "datadir": "", "user": rpcconn.rpcuser, "password": rpcconn.rpcpassword, "port": rpcconn.rpcport, "host": rpcconn.ipaddress, "protocol": "http", }, "auth": { "method": "rpcpasswordaspin", }, } specter = Specter(data_folder=devices_filled_data_folder, config=config) specter.check() # Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt. # TODO: Make a test fixture in conftest.py that sets up already funded wallets # for a bitcoin core hot wallet. wallet_manager = WalletManager( 200100, devices_filled_data_folder, rpc, "regtest", device_manager, allow_threading=False, ) # Create a new device that can sign psbts (Bitcoin Core hot wallet) device = device_manager.add_device(name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]) device.setup_device(file_password=None, wallet_manager=wallet_manager) device.add_hot_wallet_keys( mnemonic= "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", passphrase="", paths=["m/49h/0h/0h"], file_password=None, wallet_manager=wallet_manager, testnet=True, keys_range=[0, 1000], keys_purposes=[], ) wallet = wallet_manager.create_wallet("bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]) # Fund the wallet. Going to need a LOT of utxos to play with. logger.info("Generating utxos to wallet") test_address = wallet.getnewaddress( ) # 2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou wallet.rpc.generatetoaddress(1, test_address)[0] # newly minted coins need 100 blocks to get spendable # let's mine another 100 blocks to get these coins spendable trash_address = wallet.getnewaddress() wallet.rpc.generatetoaddress(100, trash_address) # the utxo is only available after the 100 mined blocks utxos = wallet.rpc.listunspent() # txid of the funding of test_address txid = utxos[0]["txid"] assert wallet._addresses[test_address]["label"] is None number_of_addresses = len(wallet._addresses) # Electrum # Test it with a txid label that does not belong to the wallet -> should be ignored wallet.import_address_labels( json.dumps({ "8d0958cb8701fac7421eb077e44b36809b90c7ad4a35e0c607c2cd591c522668": "txid label" })) assert wallet._addresses[test_address]["label"] is None assert len(wallet._addresses) == number_of_addresses # Test it with an address label that does not belong to the wallet -> should be ignored wallet.import_address_labels( json.dumps({"12dRugNcdxK39288NjcDV4GX7rMsKCGn6B": "address label"})) assert wallet._addresses[test_address]["label"] is None assert len(wallet._addresses) == number_of_addresses # Test it with a txid label wallet.import_address_labels(json.dumps({txid: "txid label"})) assert wallet._addresses[test_address]["label"] == "txid label" # The txid label should now be replaced by the address label wallet.import_address_labels( json.dumps({test_address: "address label"})) assert wallet._addresses[test_address]["label"] == "address label" # Specter JSON wallet._addresses[test_address].set_label("some_fancy_label_json") specter_json = json.dumps(wallet.to_json(for_export=True)) wallet._addresses[test_address].set_label("label_got_lost") wallet.import_address_labels(specter_json) assert wallet._addresses[test_address][ "label"] == "some_fancy_label_json" # Specter CSV csv_string = """Index,Address,Type,Label,Used,UTXO,Amount (BTC) 0,2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou,receive,some_fancy_label_csv,Yes,0,0""" wallet._addresses[test_address].set_label("label_got_lost") wallet.import_address_labels(csv_string) assert wallet._addresses[test_address][ "label"] == "some_fancy_label_csv" finally: # Clean up bitcoind_controller.stop_bitcoind()
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): assert bitcoin_regtest.get_rpc().test_connection() config = { "rpc": { "autodetect": False, "datadir": "", "user": bitcoin_regtest.rpcconn.rpcuser, "password": bitcoin_regtest.rpcconn.rpcpassword, "port": bitcoin_regtest.rpcconn.rpcport, "host": bitcoin_regtest.rpcconn.ipaddress, "protocol": "http", }, "auth": { "method": "rpcpasswordaspin", }, } specter = Specter(data_folder=devices_filled_data_folder, config=config) assert specter.chain == "regtest" # Create a User someuser = specter.user_manager.add_user( User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password("somepassword"), "config": {}, "is_admin": False, "services": None, }, specter=specter, )) specter.user_manager.save() specter.check() assert not someuser.wallet_manager.working_folder is None # Create a Wallet wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' wallet_importer = WalletImporter(wallet_json, specter, device_manager=someuser.device_manager) wallet_importer.create_nonexisting_signers( someuser.device_manager, { "unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor" }, ) dm: DeviceManager = someuser.device_manager wallet = wallet_importer.create_wallet(someuser.wallet_manager) try: # fund it with some coins bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress()) # make sure it's confirmed bitcoin_regtest.mine() # Realize that the wallet has funds: wallet.update() except SpecterError as se: if str(se).startswith("Timeout"): pytest.fail( "We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!" ) return else: raise se assert wallet.fullbalance >= 20 assert not specter.wallet_manager.working_folder is None try: yield specter finally: # Deleting all Wallets (this will also purge them on core) for user in specter.user_manager.users: for wallet in list(user.wallet_manager.wallets.values()): user.wallet_manager.delete_wallet( wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest")