Skip to content

Activate BIP44 wallet derivation - #124

Merged
Bitflash-sh merged 6 commits into
Bitflash-sh:mainfrom
mtg0001:bip44-wallet-activation
Aug 3, 2026
Merged

Activate BIP44 wallet derivation#124
Bitflash-sh merged 6 commits into
Bitflash-sh:mainfrom
mtg0001:bip44-wallet-activation

Conversation

@mtg0001

@mtg0001 mtg0001 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make newly installed recovery phrases use the BIP44 schema with Bitflash's provisional coin type: m/44'/4346950'/0'/0/index for receiving addresses.
  • Derive transaction change from the BIP44 internal chain: m/44'/4346950'/0'/1/index.
  • Keep existing legacy-hd wallets on their recorded m/index' schema.
  • Teach restore/audit to cover BIP44 receive, BIP44 change, and the legacy-HD compatibility branch so older phrase wallets remain recoverable.
  • Extend wallet HD self-tests to prove the default receive path, receive/change separation, receive pool counters, and legacy-HD phrase compatibility.

Why

This is the first PR in the stack that turns the BIP44 preparation into actual wallet behavior. It keeps the old schema alive for existing wallets, while new recovery phrases start using the standard BIP44 receive/change split.

The compatibility branch matters because wallets created before this change used m/index'. A new build must not make those old phrases look empty during restore.

Stack

Stacked after #121, #122, and #123.

Testing

  • git diff --check
  • mingw32-make -f makefile.mingw tests from MSYS2 UCRT64 in src/
  • Manual disposable-datadir smoke test on this branch:
    • /newphrase succeeds
    • restart via a separate CLI invocation
    • /recoveryaudit reports derivation schema: bip44, BIP44 coin type: 4346950 (provisional BITFLASH), and receive/change counters 101/0
    • /showderived=3 derives the first three receiving addresses
  • Restore on an empty-height disposable datadir still refuses before touching the wallet, as intended by the scan guard.

Not Yet

This is still not a request for the official SLIP-0044 registry. Before asking SatoshiLabs for 4346950 | BITFLASH | Bitflash, this should be audited and then tested with real local-chain coins: receive outputs, change outputs, gap behavior, and phrase-only restore on a synced/non-empty chain.

@mtg0001

mtg0001 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Additional live-chain smoke test on the PR branch, using a disposable datadir and no real wallet files:

  • Created a fresh BIP44 wallet on this branch.
  • Generated receiving address BJvgMruVV6fF7ezZ4sUJb4wi6FCh1qhzyd.
  • Received a real 1.00 BTF payment on the live chain.
  • Synced the disposable node from height 0 to the network tip reported by peers.
  • Ran /rescan + /recoveryaudit after sync:
    • derivation schema: bip44
    • BIP44 coin type: 4346950 (provisional BITFLASH)
    • total spendable balance: 1.00 BTF
    • covered by recovery phrase: 1.00 BTF (1 transaction)
    • wallet.dat-only balance: 0.00 BTF
  • Moved the disposable wallet.dat aside and restored only from the twelve-word phrase in the same synced datadir.
  • Restore recovered the payment from chain scan:
    • Restored: 1 transaction(s) across 1201 derived addresses.
    • post-restore audit again showed 1.00 BTF fully covered by the phrase.
  • Sent 0.10 BTF to a new address from the restored wallet to exercise BIP44 change derivation.
    • /sendto succeeded with tx 3a54e50807d6967270807b2415405ce199a86828a7adc7dd48a7dd57c0c4113d.
    • receive/change counters advanced from 502/400 to 502/401.
    • audit still showed wallet.dat-only balance: 0.00 BTF.
    • the node reconnected to peers, accepted the tx locally, relayed it, and peers requested it via getdata.

Remaining before any SLIP-0044 request: a mined change-output restore where the spend is confirmed in a later block, ideally sending to a second disposable wallet so the restored source wallet's remaining balance proves the change branch unambiguously.

@mtg0001

mtg0001 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Manual restore test update after the external-spend/change check:

  • funded a BIP44 wallet from the live chain with 1.00 BTF
  • restored from the twelve words only: recovered the funding transaction and audit reported 1.00 BTF phrase-backed, 0.00 BTF wallet.dat-only
  • sent 0.20 BTF to a second disposable BIP44 wallet; the source wallet created BIP44 change and advanced the change counter
  • tx 652e85d8ef717d5a4e8c3b53601d661292ab5ee03a398857980a2b3fb7e0c0d0 was mined in block 6ac71b90746767 at height 6505
  • restoring the source wallet again from the twelve words exposed a bug: the restore depth was counted across receive + change + legacy compatibility branches, so -restoredepth=600 could stop before scanning 600 change addresses
  • fixed in 08bde53 by requiring BIP44 restore depth per branch, with self-test coverage for the stop condition

After the fix, the same wallet restored from phrase only reports:

Restored: 3 transaction(s) across 2401 derived addresses.
Wallet recovery audit
  recovery phrase: present
  derivation schema: bip44
  BIP44 coin type: 4346950 (provisional BITFLASH)
  derived keys known to this wallet: 2501
  receive/change counters: 901/800
  total spendable balance:      0.80 BTF
  covered by recovery phrase:   0.80 BTF (1 transaction(s))
  wallet.dat-only balance:      0.00 BTF (0 transaction(s))

Automated tests rerun after the fix:

mingw32-make -f makefile.mingw tests
ALL TESTS PASSED

@mtg0001

mtg0001 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

One more restore edge case was tested before audit.

The previous manual proof used -restoredepth=600 and passed, but a normal user will often restore with the default depth. Running the same mined-change restore without an explicit depth exposed that the default window could still stop before the later change branch output.

Added d7bcdd1:

  • keeps the BIP44 depth rule per branch from 08bde53
  • widens the default restore lookahead enough for the ordinary sequence: restore from phrase, spend, receive change, restore from phrase again
  • leaves explicit -restoredepth=N available for deeper scans

Retested without -restoredepth against the same live-chain fixture:

Restored: 3 transaction(s) across 2401 derived addresses.
Wallet recovery audit
  recovery phrase: present
  derivation schema: bip44
  BIP44 coin type: 4346950 (provisional BITFLASH)
  derived keys known to this wallet: 2501
  receive/change counters: 901/800
  total spendable balance:      0.80 BTF
  covered by recovery phrase:   0.80 BTF (1 transaction(s))
  wallet.dat-only balance:      0.00 BTF (0 transaction(s))

Automated tests still pass:

mingw32-make -f makefile.mingw tests
ALL TESTS PASSED

@Bitflash-sh

Copy link
Copy Markdown
Owner

Merging this stack. It is the first time wallet derivation has come through this cleanly here, and the compatibility design is the reason: the schema is recorded per wallet, wallets that predate the field are inferred as legacy, nobody is migrated behind their back, and restore walks the legacy branch alongside the BIP44 ones. That is the right shape.

I did not take the self-tests as proof. This area has burned us before — #115 passed every test on the bench and still could not find real money — so it was tested against real coin on a real chain.

Compatibility, on a real wallet

The wallet from the #115 proof: a phrase installed, 701 derived indices, coins at 201 and 302.

v1.2.15 this stack
derived addresses 0–5 B7kQD9…, BEhLSj…, BMwR1i…, BRuf52…, BMXA4q…, BJ2Hmu… identical
audit 5.00 BTF, all phrase-covered 5.00 BTF, all phrase-covered
schema reported legacy-hd

An upgraded wallet keeps every address it had. That was the thing that had to be true.

The case that matters most: an existing user restoring after the upgrade

Real chain, no wallet.dat, only the twelve words from a phrase created under the old layout:

  1801 addresses checked, 2 transaction(s) recovered
Restored: 2 transaction(s) across 1801 derived addresses.

  total spendable balance:      5.00 BTF
  covered by recovery phrase:   5.00 BTF (1 transaction(s))

The legacy compatibility branch does its job with real money on the other end.

And the new branch, with coin that actually moved

The one thing derivation tests cannot show is whether the scan finds a coin that was really paid to a BIP44 address. So one was:

  1. New wallet, new phrase — comes out bip44, coin type 4346950.
  2. -newaddressBAtgSgra2X8Dqo5PJzPCLJhPwrrC2thYVJ, confirmed to be receive-branch index 1 and not a stray random key.
  3. 1.00 BTF sent to it from another wallet, mined into the chain, three blocks deep.
  4. wallet.dat deleted.
  5. Restored from the twelve words alone: 1 transaction(s) recovered, 1.00 BTF back, audit reports it phrase-covered.

Coin fully accounted for on both sides afterwards: 4.00 in the sender as change, 1.00 in the restored wallet.

Also built the merge against current main and compiled it before trusting it — a clean git merge is not a compiling merge in this tree.

Two things that are now owed, and neither is a code defect

1. The coin type is provisional. 4346950 has no SLIP-0044 assignment; the comment says "proposed registry row". Once this ships, every phrase created under it depends on that number permanently. If a different one is ever assigned, the provisional path cannot be replaced — it can only be kept as a fourth compatibility branch, the same way the legacy path is kept now. That is a maintenance obligation we are taking on knowingly, and I am opening an issue for it rather than letting it live only in a comment.

2. A phrase created here restores to nothing on 1.2.15 and earlier. The words carry no schema, so an older build derives the legacy path and finds an empty wallet. Anyone running more than one machine can hit this by restoring on the one they have not upgraded — and "my coins are gone" is exactly the wrong thing for a recovery phrase to say. This needs to be loud in the release notes, and the release should wait until the machines that matter are already on it.

One observation, not a blocker: restore now derives three branches per batch, so an ordinary case wrote 1501–1801 keys to find one or two transactions. It works, it is just heavier, and each future compatibility branch multiplies it again.

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.

2 participants