Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2025-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or https://www.opensource.org/licenses/mit-license.php.6import os8from test_framework.descriptors import descsum_create9from test_framework.key import H_POINT10from test_framework.test_framework import BitcoinTestFramework11from test_framework.util import (12assert_equal,13assert_not_equal,14assert_raises_rpc_error,15)16from test_framework.wallet_util import generate_keypair18KEYPOOL_SIZE = 1020class WalletExportedWatchOnly(BitcoinTestFramework):21def set_test_params(self):22self.setup_clean_chain = True23self.num_nodes = 224self.extra_args = [[], [f"-keypool={KEYPOOL_SIZE}"]]26def setup_network(self):27# Setup the nodes but don't connect them to each other28self.setup_nodes()30def skip_test_if_missing_module(self):31self.skip_if_no_wallet()33def export_and_restore(self, wallet, export_name):34export_path = os.path.join(self.export_path, f"{export_name}.dat")35res = wallet.exportwatchonlywallet(export_path)36assert_equal(res["exported_file"], export_path)37self.online.restorewallet(export_name, res["exported_file"])38return self.online.get_wallet_rpc(export_name)40def test_basic_export(self):41self.log.info("Test basic watchonly wallet export")42self.offline.createwallet("basic")43offline_wallet = self.offline.get_wallet_rpc("basic")45# Bad RPC args46assert_raises_rpc_error(-4, "Error: Export ", offline_wallet.exportwatchonlywallet, "")47assert_raises_rpc_error(-4, "Error: Export destination '.' already exists", offline_wallet.exportwatchonlywallet, ".")48assert_raises_rpc_error(-4, f"Error: Export destination '{self.export_path}' already exists", offline_wallet.exportwatchonlywallet, self.export_path)50# Export the watchonly wallet file and load onto online node51online_wallet = self.export_and_restore(offline_wallet, "basic_watchonly")53# Exporting watchonly from a watchonly also works54online_wallet2 = self.export_and_restore(online_wallet, "basic_watchonly2")56# Verify that the wallets have the same descriptors57addr = offline_wallet.getnewaddress()58assert_equal(addr, online_wallet.getnewaddress())59assert_equal(addr, online_wallet2.getnewaddress())60assert_equal(offline_wallet.listdescriptors()["descriptors"], online_wallet.listdescriptors()["descriptors"])61assert_equal(offline_wallet.listdescriptors()["descriptors"], online_wallet2.listdescriptors()["descriptors"])63# Verify that online wallet cannot spend, but offline can64self.funder.sendtoaddress(online_wallet.getnewaddress(), 10)65self.generate(self.online, 1, sync_fun=self.no_op)66assert_equal(online_wallet.getbalances()["mine"]["trusted"], 10)67assert_equal(offline_wallet.getbalances()["mine"]["trusted"], 0)68funds_addr = self.funder.getnewaddress()69send_res = online_wallet.send([{funds_addr: 5}])70assert_equal(send_res["complete"], False)71assert "psbt" in send_res72signed_psbt = offline_wallet.walletprocesspsbt(send_res["psbt"])["psbt"]73finalized = self.online.finalizepsbt(signed_psbt)["hex"]74self.online.sendrawtransaction(finalized)76# Verify that the change address is known to both wallets77dec_tx = self.online.decoderawtransaction(finalized)78for txout in dec_tx["vout"]:79if txout["scriptPubKey"]["address"] == funds_addr:80continue81assert_equal(online_wallet.getaddressinfo(txout["scriptPubKey"]["address"])["ismine"], True)82assert_equal(offline_wallet.getaddressinfo(txout["scriptPubKey"]["address"])["ismine"], True)84# Both wallets should agree on addresses generated past the end of the keypool85offline_wallet.getnewaddress()86offline_wallet.getrawchangeaddress()87for _ in range(KEYPOOL_SIZE * 2):88assert_equal(online_wallet.getnewaddress(), offline_wallet.getnewaddress())89assert_equal(online_wallet.getrawchangeaddress(), offline_wallet.getrawchangeaddress())91self.generate(self.online, 1, sync_fun=self.no_op)92offline_wallet.unloadwallet()93online_wallet.unloadwallet()95def test_export_with_address_book(self):96self.log.info("Test all address book entries appear in the exported wallet")97self.offline.createwallet("addrbook")98offline_wallet = self.offline.get_wallet_rpc("addrbook")100# Create some address book entries101receive_addr = offline_wallet.getnewaddress(label="addrbook_receive")102send_addr = self.funder.getnewaddress()103offline_wallet.setlabel(send_addr, "addrbook_send") # Sets purpose "send"105# Export the watchonly wallet file and load onto online node106online_wallet = self.export_and_restore(offline_wallet, "addrbook_watchonly")108# Verify the labels are in both wallets109for wallet in [online_wallet, offline_wallet]:110for purpose in ["receive", "send"]:111label = f"addrbook_{purpose}"112assert_equal(wallet.listlabels(purpose), [label])113addr = send_addr if purpose == "send" else receive_addr114assert_equal(wallet.getaddressesbylabel(label), {addr: {"purpose": purpose}})116offline_wallet.unloadwallet()117online_wallet.unloadwallet()119def test_export_with_txs_and_locked_coins(self):120self.log.info("Test all transactions and locked coins appear in the exported wallet")121self.offline.createwallet("txs")122offline_wallet = self.offline.get_wallet_rpc("txs")124# In order to make transactions in the offline wallet, briefly connect offline to online125self.connect_nodes(self.offline.index, self.online.index)126txids = [self.funder.sendtoaddress(offline_wallet.getnewaddress("funds"), i) for i in range(1, 4)]127self.generate(self.online, 1)128self.disconnect_nodes(self.offline.index, self.online.index)130# lock some coins131utxos = offline_wallet.listunspent()132persistent_lock = [{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"]}]133temp_lock = [{"txid": utxos[1]["txid"], "vout": utxos[1]["vout"]}]134offline_wallet.lockunspent(unlock=False, transactions=persistent_lock, persistent=True)135offline_wallet.lockunspent(unlock=False, transactions=temp_lock, persistent=False)137# Export the watchonly wallet file and load onto online node138# There should not be a rescan as the nodes were at the same height139with self.online.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning last"]):140online_wallet = self.export_and_restore(offline_wallet, "txs_watchonly")142# Verify the transactions are in both wallets143for txid in txids:144assert_equal(online_wallet.gettransaction(txid), offline_wallet.gettransaction(txid))146# Verify the transactions are all output by listtransactions, in the same order147assert_equal(online_wallet.listtransactions(), offline_wallet.listtransactions())149# New transactions should be the newest (last) transaction in listtransactions150new_txid = self.funder.sendtoaddress(online_wallet.getnewaddress(), 1)151online_wallet.syncwithvalidationinterfacequeue()152assert_equal(online_wallet.listtransactions()[-1]["txid"], new_txid)154# Verify that the persistent locked coin is locked in both wallets155assert_equal(online_wallet.listlockunspent(), persistent_lock)156assert_equal(sorted(offline_wallet.listlockunspent(), key=lambda x: x["txid"]), sorted(persistent_lock + temp_lock, key=lambda x: x["txid"]))158offline_wallet.unloadwallet()159online_wallet.unloadwallet()161def test_export_imported_descriptors(self):162self.log.info("Test imported descriptors are exported to the watchonly wallet")163self.offline.createwallet("imports")164offline_wallet = self.offline.get_wallet_rpc("imports")166import_res = offline_wallet.importdescriptors(167[168# A single key, non-ranged169{"desc": descsum_create(f"pkh({generate_keypair(wif=True)[0]})"), "timestamp": "now"},170# hardened derivation171{"desc": descsum_create("sh(wpkh(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/0'/*'))"), "timestamp": "now", "active": True},172# multisig173{"desc": descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*,tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/*))"), "timestamp": "now", "active": True, "internal": True},174# taproot multi scripts175{"desc": descsum_create(f"tr({H_POINT},{{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*),pk(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/0h/*)}})"), "timestamp": "now", "active": True},176# miniscript177{"desc": descsum_create(f"tr({H_POINT},or_b(pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/2/*),s:pk(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/1h/2/*)))"), "timestamp": "now", "active": True, "internal": True},178]179)180assert_equal(all([r["success"] for r in import_res]), True)182# Export the watchonly wallet file and load onto online node183online_wallet = self.export_and_restore(offline_wallet, "imports_watchonly")185# Verify public descriptors are the same186assert_equal(offline_wallet.listdescriptors()["descriptors"], online_wallet.listdescriptors()["descriptors"])188# Verify all the addresses are the same189for address_type in ["legacy", "p2sh-segwit", "bech32", "bech32m"]:190for internal in [False, True]:191if internal:192addr = offline_wallet.getrawchangeaddress(address_type=address_type)193assert_equal(addr, online_wallet.getrawchangeaddress(address_type=address_type))194else:195addr = offline_wallet.getnewaddress(address_type=address_type)196assert_equal(addr, online_wallet.getnewaddress(address_type=address_type))197self.funder.sendtoaddress(addr, 1)198self.generate(self.online, 1, sync_fun=self.no_op)200# The hardened derivation should have KEYPOOL_SIZE - 1 remaining addresses201for _ in range(KEYPOOL_SIZE - 1):202online_wallet.getnewaddress(address_type="p2sh-segwit")203assert_raises_rpc_error(-12, "No addresses available", online_wallet.getnewaddress, address_type="p2sh-segwit")205# Verify that the offline wallet can sign and send206send_res = online_wallet.sendall([self.funder.getnewaddress()])207assert_equal(send_res["complete"], False)208assert "psbt" in send_res209signed_psbt = offline_wallet.walletprocesspsbt(send_res["psbt"])["psbt"]210finalized = self.online.finalizepsbt(signed_psbt)["hex"]211self.online.sendrawtransaction(finalized)213self.generate(self.online, 1, sync_fun=self.no_op)214offline_wallet.unloadwallet()215online_wallet.unloadwallet()217def test_avoid_reuse(self):218self.log.info("Test that the avoid reuse flag appears in the exported wallet")219self.offline.createwallet(wallet_name="avoidreuse", avoid_reuse=True)220offline_wallet = self.offline.get_wallet_rpc("avoidreuse")221assert_equal(offline_wallet.getwalletinfo()["avoid_reuse"], True)223# The avoid_reuse flag also sets some specific address book entries to track reused addresses224# In order for these to be set, a few transactions need to be made, so briefly connect offline to online225self.connect_nodes(self.offline.index, self.online.index)226addr = offline_wallet.getnewaddress()227self.funder.sendtoaddress(addr, 1)228self.generate(self.online, 1)229# Spend funds in order to mark addr as previously spent230offline_wallet.sendall([self.funder.getnewaddress()])231self.funder.sendtoaddress(addr, 1)232self.generate(self.online, 1)233assert_equal(offline_wallet.listunspent(addresses=[addr])[0]["reused"], True)234self.disconnect_nodes(self.offline.index, self.online.index)236# Export the watchonly wallet file and load onto online node237online_wallet = self.export_and_restore(offline_wallet, "avoidreuse_watchonly")239# check avoid_reuse is still set240assert_equal(online_wallet.getwalletinfo()["avoid_reuse"], True)241assert_equal(online_wallet.listunspent(addresses=[addr])[0]["reused"], True)243offline_wallet.unloadwallet()244online_wallet.unloadwallet()246def test_encrypted_wallet(self):247self.log.info("Test that a watchonly wallet can be exported from a locked wallet")248self.offline.createwallet(wallet_name="encrypted", passphrase="pass")249offline_wallet = self.offline.get_wallet_rpc("encrypted")250assert_equal(offline_wallet.getwalletinfo()["unlocked_until"], 0)
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.