upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/walletcmd.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 17:57:07 -0300
committerGitHub <noreply@github.com>2026-08-01 17:57:07 -0300
commit7de7ef89d3365f8f9c2eb2b6ed0193c3033e221c (patch)
treedba509ae6b245be76b88ad0ae467aeb997a85b4d /src/walletcmd.cpp
parentce59d8c72c667ce638bd12f37db26d8b3408a5d2 (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.cpp237
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.
26static const int RESTORE_BATCH = 100;
27static const int RESTORE_MAX = 10000;
28
29int 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
73bool 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
157static 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
163int 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
192int 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".
212int 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}