Skip to content

Conversation

@w0xlt
Copy link
Contributor

@w0xlt w0xlt commented Aug 13, 2025

This PR adds test coverage for migrating legacy Bitcoin Core wallets from v0.14.3 (released in 2017) to the descriptor wallet format. The test validates that users can safely upgrade their wallets while preserving all funds, transaction history, and addresses.

This test was originally developed on top of #32977, as it was requested in reviews.
However, since it also increases test coverage, it can be merged independently.

The test covers two wallet migration scenarios:

  • Non-HD Wallet Migration - Tests migration of non-HD wallets (created with -usehd=0)
  • Single Chain HD Wallet Migration - Tests migration of HD wallets from v0.14.3 (VERSION_HD_BASE)

The node v0.14.3 cannot be synced because it doesn't have the syncwithvalidationinterfacequeue RPC implemented (required by test_framework.py), so the solution is to copy the block folder to the modern node and start it with -reindex-chainstate. Because of this additional complexity, this testing is best managed in a separate file rather than in the existing migration test files.

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 13, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33186.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK pablomartin4btc, furszy

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

No conflicts as of last run.

LLM Linter (✨ experimental)

Possible places where comparison-specific test macros should replace generic comparisons:

  • test/functional/wallet_ancient_migration.py "assert old_balance > 0" -> use assert_greater_than(old_balance, 0)
  • test/functional/wallet_ancient_migration.py "assert len(old_txs) > 0" -> use assert_greater_than(len(old_txs), 0)
  • test/functional/wallet_ancient_migration.py "assert len(txid) == 64" -> use assert_equal(len(txid), 64)

2026-01-16

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/runs/48028643718
LLM reason (✨ experimental): The CI failure is caused by a lint error due to trailing whitespace in the code.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@w0xlt w0xlt force-pushed the test_no_or_base_hd_wallet_migration branch 3 times, most recently from 5237377 to 3b1b154 Compare August 13, 2025 19:12

# Setup fresh nodes for this test
self.add_nodes(
2,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2,
self.num_nodes,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks.

Copy link
Member

@pablomartin4btc pablomartin4btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

Left a couple of suggestions.

# Create base test directory
tmpdir = "%s/test_runner_₿_🏃_%s" % (args.tmpdirprefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S"))
# Special case for tests using old binaries (e.g. version v0.14.3) that don't handle Unicode on Windows
has_migration_tests = any('wallet_ancient_migration' in script for script in BASE_SCRIPTS)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more than has_migration_tests shouldn't indicate like it's an old binary? (there are other tests that uses migration and feature_unsupported_utxo_db.py uses a node v0.14.3 as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, the Windows, test cross-built CI task will fail because it can't handle non-ASCII characters in -datadir.

feature_unsupported_utxo_db.py doesn't copy the directory from one node to another. This test needs to do this because version 0.14.3 doesn't have the syncwithvalidationinterfacequeue RPC, so it can't be synchronized.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's already well explained in the PR description. Then perhaps the variable name could be supports_unicode_datadir/ (requires_ascii_datadir) or something like that.

Suggested change
has_migration_tests = any('wallet_ancient_migration' in script for script in BASE_SCRIPTS)
requires_ascii_datadir = any('wallet_ancient_migration' in script for script in BASE_SCRIPTS)
tmpdirprefix = "%s/test_runner_₿_🏃_%s"
if platform.system() == 'Windows' and requires_ascii_datadir:
tmpdir = "%s/test_runner_btc_run_%s"
tmpdir = tmpdirprefix % (args.tmpdirprefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S"))

Wouldn't this affect the datadir for all the tests running on Windows platform?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. It's much clearer.
Yes, it affects all tests run on the Windows platform. As far as I know, there's no way to filter by this task specifically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature_unsupported_utxo_db.py doesn't copy the directory from one node to another. This test needs to do this because version 0.14.3 doesn't have the syncwithvalidationinterfacequeue RPC, so it can't be synchronized.

What's the error message? It seems odd that one test works fine with unicode and the other does not. See also the suggestion https://github.com/bitcoin/bitcoin/pull/33186/files#r2281561637

Copy link
Member

@furszy furszy Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature_unsupported_utxo_db.py doesn't copy the directory from one node to another. This test needs to do this because version 0.14.3 doesn't have the syncwithvalidationinterfacequeue RPC, so it can't be synchronized.

What's the error message? It seems odd that one test works fine with unicode and the other does not. See also the suggestion https://github.com/bitcoin/bitcoin/pull/33186/files#r2281561637

@w0xlt can you please check this ^^ ?

Also, aren't we essentially removing the unicode coverage from the CI with this change? wallet_ancient_migration.py test should always be enabled there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the same approach as used in #34240

Comment on lines 134 to 177
# Stop the old node
self.log.info("Stopping old node...")
self.stop_node(0)

# Copy blockchain data from node0 to node1
self.log.info("Copying blockchain data from old node to modern node...")
old_blocks_dir = os.path.join(node0_datadir, self.chain, "blocks")
new_blocks_dir = os.path.join(node1_datadir, self.chain, "blocks")

# Clean up any existing blocks directory in node1
if os.path.exists(new_blocks_dir):
shutil.rmtree(new_blocks_dir)

if os.path.exists(old_blocks_dir):
shutil.copytree(old_blocks_dir, new_blocks_dir)
self.log.info(" Copied blocks directory")
assert(os.path.exists(new_blocks_dir))

# Start the modern node with -reindex-chainstate
self.log.info("Starting modern node with -reindex-chainstate...")
self.start_node(1, extra_args=["-reindex-chainstate"])

# Wait for reindex to complete
self.log.info("Waiting for chainstate reindex to complete...")
self.wait_until(
lambda: self.nodes[1].getblockcount() == 102,
timeout=30
)

# Verify the blockchain was loaded correctly
node1_info = self.nodes[1].getblockchaininfo()
self.log.info(f"Modern node blockchain info: height={node1_info['blocks']}")
assert_equal(node1_info['blocks'], 102)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may be able to skip this by using dumb_sync_blocks from c847dee#diff-e48d83ef13f93a84b5686eb797eeed80dd194cf39db7f2972cf82a5ed25415a7L78-L95

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find reference to dumb_sync_blocks in that commit.
I used the same approach as in feature_unsupported_utxo_db.py

@fanquake
Copy link
Member

fanquake commented Dec 3, 2025

@achow101 can you leave a conceptual review?

@furszy
Copy link
Member

furszy commented Dec 3, 2025

Concept ACK, will review soon

Copy link
Member

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node v0.14.3 cannot be synced because it doesn't have the syncwithvalidationinterfacequeue RPC implemented (required by test_framework.py), so the solution is to copy the block folder to the modern node and start it with -reindex-chainstate. Because of this additional complexity, this testing is best managed in a separate file rather than in the existing migration test files.

In order to know when the node is synced, cannot you just wait for a specific log line? See busy_wait_for_debug_log.

# Create base test directory
tmpdir = "%s/test_runner_₿_🏃_%s" % (args.tmpdirprefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S"))
# Special case for tests using old binaries (e.g. version v0.14.3) that don't handle Unicode on Windows
has_migration_tests = any('wallet_ancient_migration' in script for script in BASE_SCRIPTS)
Copy link
Member

@furszy furszy Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature_unsupported_utxo_db.py doesn't copy the directory from one node to another. This test needs to do this because version 0.14.3 doesn't have the syncwithvalidationinterfacequeue RPC, so it can't be synchronized.

What's the error message? It seems odd that one test works fine with unicode and the other does not. See also the suggestion https://github.com/bitcoin/bitcoin/pull/33186/files#r2281561637

@w0xlt can you please check this ^^ ?

Also, aren't we essentially removing the unicode coverage from the CI with this change? wallet_ancient_migration.py test should always be enabled there.

Comment on lines 86 to 95
old_wallet_info = self.nodes[0].getwalletinfo()
hdmasterkeyid = old_wallet_info.get('hdmasterkeyid', 'Not HD')
self.log.info(f"Old wallet info: HD master key = {hdmasterkeyid}")
assert('keypoolsize_hd_internal' not in old_wallet_info) # Ensure we are testing non-HD or single-chain (VERSION_HD_BASE) wallets

# Verify wallet type
if "non-HD" in wallet_type:
assert('hdmasterkeyid' not in old_wallet_info)
else:
assert('hdmasterkeyid' in old_wallet_info)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could check the wallet version here.
The non-hd one should have version 6000 (WalletFeature::FEATURE_COMPRPUBKEY) and the hd one should have version 130000 (WalletFeature::FEATURE_HD)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks.

Comment on lines 193 to 195
# Rescan the blockchain
self.log.info("Rescanning blockchain...")
self.nodes[1].rescanblockchain()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rescanning after migration shouldn't be needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks.

Test migratewallet RPC on ancient wallet formats from v0.14.3:
- Non-HD wallet (FEATURE_COMPRPUBKEY, version 60000)
- HD single-chain wallet (VERSION_HD_BASE, version 130000)

Verifies balance, transactions, and address ownership are preserved
after migration to descriptor wallets.
@w0xlt w0xlt force-pushed the test_no_or_base_hd_wallet_migration branch from 17d091e to 1b6f3f1 Compare January 16, 2026 20:49
@w0xlt
Copy link
Contributor Author

w0xlt commented Jan 16, 2026

Updated the PR with the same solution used in #34240.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants