diff options
| author | Bitflash <btf01@proton.me> | 2026-08-01 17:57:07 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 17:57:07 -0300 |
| commit | 7de7ef89d3365f8f9c2eb2b6ed0193c3033e221c (patch) | |
| tree | dba509ae6b245be76b88ad0ae467aeb997a85b4d /src/walletcmd.cpp | |
| parent | ce59d8c72c667ce638bd12f37db26d8b3408a5d2 (diff) | |
wallet: create and restore a recovery phrase (#96)
Adds -newphrase, -restorephrase, -restoredepth, -newaddress and -showderived, plus the window buttons that reach the same code. Restore derives forward in batches of 100 and rescans after each, stopping when a whole batch turns up nothing; the phrase is shown on the terminal only, never in debug.log. Proved end to end on a live wallet: paid a derived address, deleted wallet.dat, restored from the twelve words and recovered the coin. Coins on the pre-seed default address do not come back, and the command says so.
Diffstat (limited to 'src/walletcmd.cpp')
| -rw-r--r-- | src/walletcmd.cpp | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp new file mode 100644 index 0000000..94e9d72 --- /dev/null +++ b/src/walletcmd.cpp | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // Recovery-phrase commands, from the command line. | ||
| 5 | // | ||
| 6 | // The phrase reaches the person who typed the command and nowhere else. In this | ||
| 7 | // tree printf is OutputDebugStringF, which writes into the data directory's | ||
| 8 | // debug.log -- the file we routinely ask users to attach to issues. A wallet's | ||
| 9 | // twelve words must never go there, so this file takes the real printf and | ||
| 10 | // AttachTerminal() gives it somewhere to land on Windows. | ||
| 11 | |||
| 12 | #include "headers_core.h" | ||
| 13 | #include "bip32.h" | ||
| 14 | #include "walletcmd.h" | ||
| 15 | |||
| 16 | #undef printf | ||
| 17 | |||
| 18 | // How far ahead to look for used addresses when restoring. | ||
| 19 | // | ||
| 20 | // A wallet is restored from a phrase alone, so the node has no idea how many | ||
| 21 | // addresses the old one handed out. It derives forward in batches and stops | ||
| 22 | // once a whole batch turns up unused. A fixed number would be simpler and wrong | ||
| 23 | // for exactly this network: a miner burns an address per block, so a node that | ||
| 24 | // mined for a week is thousands of keys deep, and stopping at a few hundred | ||
| 25 | // would restore a wallet that looks empty. | ||
| 26 | static const int RESTORE_BATCH = 100; | ||
| 27 | static const int RESTORE_MAX = 10000; | ||
| 28 | |||
| 29 | int CmdNewPhrase() | ||
| 30 | { | ||
| 31 | AttachTerminal(); | ||
| 32 | |||
| 33 | if (HaveHDSeed()) | ||
| 34 | { | ||
| 35 | printf("This wallet already has a recovery phrase.\n"); | ||
| 36 | printf("Creating another would replace it, and coins on addresses derived from\n"); | ||
| 37 | printf("the current one would no longer come back from the phrase you write down.\n"); | ||
| 38 | printf("Nothing was changed.\n"); | ||
| 39 | return 1; | ||
| 40 | } | ||
| 41 | |||
| 42 | std::string strMnemonic, strError; | ||
| 43 | if (!bitflash::BIP39GenerateMnemonic(strMnemonic, strError)) | ||
| 44 | { | ||
| 45 | fprintf(stderr, "Could not generate a phrase: %s\n", strError.c_str()); | ||
| 46 | return 1; | ||
| 47 | } | ||
| 48 | |||
| 49 | if (!SetHDSeedFromMnemonic(strMnemonic, strError)) | ||
| 50 | { | ||
| 51 | fprintf(stderr, "Could not install the seed: %s\n", strError.c_str()); | ||
| 52 | return 1; | ||
| 53 | } | ||
| 54 | |||
| 55 | // Fill the pool from the new seed straight away, so the addresses this | ||
| 56 | // wallet hands out next are ones the phrase can reproduce. | ||
| 57 | TopUpKeyPool(); | ||
| 58 | |||
| 59 | printf("\n"); | ||
| 60 | printf("Write these twelve words down, in order, on paper.\n"); | ||
| 61 | printf("They are the only thing that can rebuild this wallet if the file is lost.\n"); | ||
| 62 | printf("Anyone who reads them can spend your coins. They are not stored anywhere,\n"); | ||
| 63 | printf("and this is the only time they will be shown.\n"); | ||
| 64 | printf("\n"); | ||
| 65 | printf(" %s\n", strMnemonic.c_str()); | ||
| 66 | printf("\n"); | ||
| 67 | printf("Keys already in this wallet are NOT covered by the phrase -- they existed\n"); | ||
| 68 | printf("before it did. Keep your file backups as well.\n"); | ||
| 69 | fflush(stdout); | ||
| 70 | return 0; | ||
| 71 | } | ||
| 72 | |||
| 73 | bool RestoreFromPhrase(const std::string& strMnemonic, | ||
| 74 | int nMinDepth, | ||
| 75 | RestoreProgressFn fnProgress, | ||
| 76 | void* pArg, | ||
| 77 | std::string& strErrorRet, | ||
| 78 | int& nRecoveredRet, | ||
| 79 | int& nDerivedRet) | ||
| 80 | { | ||
| 81 | strErrorRet.clear(); | ||
| 82 | nRecoveredRet = 0; | ||
| 83 | nDerivedRet = 0; | ||
| 84 | |||
| 85 | // Refuse before touching the wallet, not after. | ||
| 86 | // | ||
| 87 | // Without this, a restore on a node that has not synced derives the right | ||
| 88 | // keys, scans a chain that is not there, and reports "no transactions were | ||
| 89 | // found" -- which reads as "your coins are gone" to the one person least | ||
| 90 | // able to argue with it. Measured while testing: a copied data directory | ||
| 91 | // whose Berkeley DB environment failed to load left the node at height 0, | ||
| 92 | // the scan walked one block, and the message said nothing was found across | ||
| 93 | // 600 addresses. | ||
| 94 | if (!CanScanWalletTransactions(strErrorRet)) | ||
| 95 | return false; | ||
| 96 | |||
| 97 | if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet)) | ||
| 98 | return false; | ||
| 99 | |||
| 100 | // Derive forward in batches, scanning after each, until a whole batch turns | ||
| 101 | // up nothing. Every derived key is written to the wallet before the scan, | ||
| 102 | // because the scan asks the wallet what belongs to it. | ||
| 103 | int nTotalDerived = 0; | ||
| 104 | while (nTotalDerived < RESTORE_MAX) | ||
| 105 | { | ||
| 106 | // Count wallet transactions, not scan hits. | ||
| 107 | // | ||
| 108 | // ScanForWalletTransactions reports what it added *or updated*, and it | ||
| 109 | // walks the whole chain every time, so a transaction found in the first | ||
| 110 | // batch is reported again by every batch after it. Testing this against | ||
| 111 | // a real payment produced exactly +1 per batch and a loop that ran to | ||
| 112 | // the ceiling, deriving ten thousand addresses to rediscover one coin. | ||
| 113 | size_t nWalletBefore = 0; | ||
| 114 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 115 | nWalletBefore = mapWallet.size(); | ||
| 116 | |||
| 117 | std::string strDeriveError; | ||
| 118 | for (int i = 0; i < RESTORE_BATCH; i++) | ||
| 119 | { | ||
| 120 | CKey key; | ||
| 121 | if (!DeriveHDKey(nHDNext, key, strDeriveError)) | ||
| 122 | break; | ||
| 123 | if (!AddKey(key)) | ||
| 124 | break; | ||
| 125 | nHDNext++; | ||
| 126 | nTotalDerived++; | ||
| 127 | } | ||
| 128 | CWalletDB().WriteHDNext(nHDNext); | ||
| 129 | |||
| 130 | ScanForWalletTransactions(pindexGenesisBlock); | ||
| 131 | |||
| 132 | size_t nWalletAfter = 0; | ||
| 133 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 134 | nWalletAfter = mapWallet.size(); | ||
| 135 | |||
| 136 | if (fnProgress) | ||
| 137 | fnProgress(pArg, nTotalDerived, (int)nWalletAfter); | ||
| 138 | |||
| 139 | // A whole batch with nothing in it means far enough -- unless the | ||
| 140 | // caller asked to look deeper anyway. | ||
| 141 | if (nWalletAfter == nWalletBefore && nTotalDerived >= nMinDepth) | ||
| 142 | break; | ||
| 143 | } | ||
| 144 | |||
| 145 | // The pool is rebuilt from where derivation stopped, so the next address | ||
| 146 | // this wallet hands out is one the phrase can reproduce as well. | ||
| 147 | CRITICAL_BLOCK(cs_keyPool) | ||
| 148 | mapKeyPool.clear(); | ||
| 149 | TopUpKeyPool(); | ||
| 150 | |||
| 151 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 152 | nRecoveredRet = (int)mapWallet.size(); | ||
| 153 | nDerivedRet = nTotalDerived; | ||
| 154 | return true; | ||
| 155 | } | ||
| 156 | |||
| 157 | static void PrintRestoreProgress(void*, int nDerived, int nRecovered) | ||
| 158 | { | ||
| 159 | printf(" %d addresses checked, %d transaction(s) recovered\n", nDerived, nRecovered); | ||
| 160 | fflush(stdout); | ||
| 161 | } | ||
| 162 | |||
| 163 | int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth) | ||
| 164 | { | ||
| 165 | AttachTerminal(); | ||
| 166 | |||
| 167 | printf("Checking the phrase and the chain.\n"); | ||
| 168 | fflush(stdout); | ||
| 169 | |||
| 170 | std::string strError; | ||
| 171 | int nRecovered = 0, nDerived = 0; | ||
| 172 | if (!RestoreFromPhrase(strMnemonic, nMinDepth, PrintRestoreProgress, NULL, | ||
| 173 | strError, nRecovered, nDerived)) | ||
| 174 | { | ||
| 175 | fprintf(stderr, "Cannot restore: %s\n", strError.c_str()); | ||
| 176 | fprintf(stderr, "Nothing was changed.\n"); | ||
| 177 | return 1; | ||
| 178 | } | ||
| 179 | |||
| 180 | printf("\n"); | ||
| 181 | if (nRecovered > 0) | ||
| 182 | printf("Restored: %d transaction(s) across %d derived addresses.\n", | ||
| 183 | nRecovered, nDerived); | ||
| 184 | else | ||
| 185 | printf("No transactions were found for the first %d addresses of that phrase.\n" | ||
| 186 | "If you expected coins, check the words and the order.\n", nDerived); | ||
| 187 | printf("Start the node normally to see the balance.\n"); | ||
| 188 | fflush(stdout); | ||
| 189 | return 0; | ||
| 190 | } | ||
| 191 | |||
| 192 | int CmdNewAddress() | ||
| 193 | { | ||
| 194 | AttachTerminal(); | ||
| 195 | |||
| 196 | std::vector<unsigned char> vchPubKey = GetKeyFromPool(); | ||
| 197 | if (vchPubKey.empty()) | ||
| 198 | { | ||
| 199 | fprintf(stderr, "Could not get a key from the wallet.\n"); | ||
| 200 | return 1; | ||
| 201 | } | ||
| 202 | |||
| 203 | printf("%s\n", PubKeyToAddress(vchPubKey).c_str()); | ||
| 204 | if (!HaveHDSeed()) | ||
| 205 | printf("(this wallet has no recovery phrase, so only a file backup covers this address)\n"); | ||
| 206 | fflush(stdout); | ||
| 207 | return 0; | ||
| 208 | } | ||
| 209 | |||
| 210 | // Diagnostic: print the addresses a phrase produces, so "the restore found | ||
| 211 | // nothing" can be told apart from "that address was never derived". | ||
| 212 | int CmdShowDerived(int nCount) | ||
| 213 | { | ||
| 214 | AttachTerminal(); | ||
| 215 | |||
| 216 | if (!HaveHDSeed()) | ||
| 217 | { | ||
| 218 | fprintf(stderr, "This wallet has no recovery phrase installed.\n"); | ||
| 219 | return 1; | ||
| 220 | } | ||
| 221 | if (nCount <= 0) | ||
| 222 | nCount = 20; | ||
| 223 | |||
| 224 | std::string strError; | ||
| 225 | for (int i = 0; i < nCount; i++) | ||
| 226 | { | ||
| 227 | CKey key; | ||
| 228 | if (!DeriveHDKey((unsigned int)i, key, strError)) | ||
| 229 | { | ||
| 230 | fprintf(stderr, "derivation failed at %d: %s\n", i, strError.c_str()); | ||
| 231 | return 1; | ||
| 232 | } | ||
| 233 | printf("%6d %s\n", i, PubKeyToAddress(key.GetPubKey()).c_str()); | ||
| 234 | } | ||
| 235 | fflush(stdout); | ||
| 236 | return 0; | ||
| 237 | } | ||