Skip to content

Also check change (/1) derivation paths by default for UTXO wallets#716

Open
Copilot wants to merge 2 commits intomasterfrom
copilot/add-change-address-check-feature
Open

Also check change (/1) derivation paths by default for UTXO wallets#716
Copilot wants to merge 2 commits intomasterfrom
copilot/add-change-address-check-feature

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 24, 2026

Previously only the supplied/default "receive" derivation paths (external chain, …/0) were searched, so a seed funded only on a change address would be missed. This adds automatic, default-on checking of the matching change paths (…/1) for UTXO-style wallets, with a new opt-out flag.

Path expansion (btcrecover/btcrseed.py)

  • New WalletBIP32._has_change_addresses class attribute (default True) and _expand_paths_for_change_addresses() helper that mirrors each path whose last (non-hardened) index is 0 into a sibling ending in 1, preserving the script-type/display-string tables and skipping duplicates.
  • WalletBIP32.create_from_params (and WalletAezeed.create_from_params) accept a new check_change_addresses=True kwarg and invoke the helper after _append_last_index handling, only in the non-MPK branch (MPK mode is bound to a specific chaincode so mirroring is pointless).
  • Hardened last indexes (e.g. m/44'/0'/0') are left untouched; script-type filtering runs before expansion, so disabled types never produce change paths.

Account-model wallets opt out

Set _has_change_addresses = False on: WalletEthereum, WalletEthereumValidator, WalletHederaEd25519, WalletZilliqa, WalletCardano, WalletPyCryptoHDWallet (covers Solana/Avalanche/Stellar/Tron/Polkadot/Cosmos/Tezos/SecretNetwork/MultiversX), WalletHelium, WalletRipple, WalletStacks, WalletXLM.

CLI & docs

  • New --no-check-change-addresses flag threaded through to create_from_params.
  • Short section added to docs/bip39-accounts-and-altcoins.md.

Tests (btcrecover/test/test_seeds.py)

New TestChangeAddresses class with 10 tests:

  • Structural: default expands, opt-out suppresses, explicit --pathlist still expands, no duplicates when /1 already listed, hardened indexes not rewritten, ETH never expands.
  • End-to-end: supplying only a change address (BIP44/BIP49/BIP84 BTC, BIP44 LTC) recovers the seed with the default setting and fails to recover it with check_change_addresses=False.

Behaviour

# BTC (default)
["m/44'/0'/0'/0", "m/0'/0"]
  → ["m/44'/0'/0'/0", "m/0'/0", "m/44'/0'/0'/1", "m/0'/1"]

# BTC with --no-check-change-addresses
  → ["m/44'/0'/0'/0", "m/0'/0"]

# ETH (unchanged; _has_change_addresses = False)
  → ["m/44'/60'/0'", "m/44'/60'/0'/0", "m/44'/1'/0'/0"]

@3rdIteration 3rdIteration marked this pull request as ready for review April 24, 2026 01:47
Copilot AI review requested due to automatic review settings April 24, 2026 01:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds default-on scanning of “change” (/1) derivation paths for UTXO wallets so seeds funded only via internal/change addresses can still be recovered, with an opt-out flag.

Changes:

  • Expand derivation path lists by mirroring eligible /0 paths to /1 for wallets that support change addresses.
  • Add --no-check-change-addresses CLI flag and document the behavior.
  • Add structural and end-to-end tests covering default behavior vs opt-out.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
docs/bip39-accounts-and-altcoins.md Documents default change-address path checking and the opt-out flag.
btcrecover/test/test_seeds.py Adds new tests validating path expansion behavior and recovery from change addresses.
btcrecover/btcrseed.py Implements change-path expansion, wires it into wallet creation, and adds CLI support plus wallet opt-outs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

def _run_change_address_scenario(self, wallet_type, change_address,
correct_mnemonic, pathlist_file,
check_change_addresses, **kwds):
test_path = btcrseed.load_pathlist("./derivationpath-lists/" + pathlist_file)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The test uses a relative path that depends on the current working directory, which can break when tests are executed from a different CWD (e.g., some CI runners or IDE test harnesses). Prefer resolving the path relative to the repository root or this test file (e.g., using pathlib.Path(__file__) and .parent), then passing the resulting absolute/normalized path into load_pathlist.

Copilot uses AI. Check for mistakes.
Comment on lines +1740 to +1742
self.assertEqual(
result_disabled, (False, 1),
"Seed must NOT be found when change-address checking is disabled")
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This assertion is brittle because it requires the exact (False, 1) tuple, including the attempt/count value, which may legitimately change if search behavior or iteration counts change. To make the test robust while still validating behavior, assert only that the “found” indicator is False (e.g., result_disabled[0] is False) and, if needed, separately assert the count is >= 1 rather than exactly 1.

Suggested change
self.assertEqual(
result_disabled, (False, 1),
"Seed must NOT be found when change-address checking is disabled")
self.assertIs(
result_disabled[0], False,
"Seed must NOT be found when change-address checking is disabled")
self.assertGreaterEqual(
result_disabled[1], 1,
"Seed verification should perform at least one check when change-address checking is disabled")

Copilot uses AI. Check for mistakes.
Comment thread btcrecover/btcrseed.py
parser.add_argument("--disable-bip84", action="store_true", help="Disable checking of BIP84 native SegWit (P2WPKH) addresses")
parser.add_argument("--pathlist", metavar="FILE", help="A list of derivation paths to be searched")
parser.add_argument("--no-check-change-addresses", action="store_true",
help="Disable the default behaviour of also checking matching \"change\" (internal, /1) derivation paths alongside \"receive\" (external, /0) paths for UTXO-style wallets (BTC, LTC, BCH, DASH, DGB, DOGE, GRS, MONA, VTC, etc). Has no effect for account-model wallets such as ETH, XRP, SOL, etc.")
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The help= string is very long and hard to scan/update in a single line. Consider splitting it into multiple adjacent string literals within parentheses to improve readability and reduce diff churn when help text changes.

Suggested change
help="Disable the default behaviour of also checking matching \"change\" (internal, /1) derivation paths alongside \"receive\" (external, /0) paths for UTXO-style wallets (BTC, LTC, BCH, DASH, DGB, DOGE, GRS, MONA, VTC, etc). Has no effect for account-model wallets such as ETH, XRP, SOL, etc.")
help=(
"Disable the default behaviour of also checking matching "
"\"change\" (internal, /1) derivation paths alongside "
"\"receive\" (external, /0) paths for UTXO-style wallets "
"(BTC, LTC, BCH, DASH, DGB, DOGE, GRS, MONA, VTC, etc). "
"Has no effect for account-model wallets such as ETH, XRP, "
"SOL, etc."
))

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://3rdIteration.github.io/btcrecover/pr-preview/pr-716/

Built to branch gh-pages at 2026-04-24 01:56 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

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.

3 participants