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:
Diffstat (limited to 'src/walletcmd.cpp')
-rw-r--r--src/walletcmd.cpp517
1 files changed, 517 insertions, 0 deletions
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
new file mode 100644
index 0000000..06f35a5
--- /dev/null
+++ b/src/walletcmd.cpp
@@ -0,0 +1,517 @@
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// Quiet batches required before giving up. Three, because the wallet's own
29// bookkeeping can leave a gap of two hundred used-nothing indices between one
30// used address and the next -- see the note at the stop condition. One was not
31// enough and cost a real balance in testing.
32static const int RESTORE_QUIET_BATCHES = 3;
33// The wallet may have restored once, scanned a few quiet batches, and then
34// spent after that. Change starts from wherever that restore stopped, so the
35// next real change output can be just beyond the old quiet window. The default
36// restore has to look far enough for that ordinary "restore, then spend, then
37// restore again" path; explicit -restoredepth can still go deeper.
38static const int RESTORE_MIN_SCAN =
39 KEYPOOL_SIZE + (RESTORE_QUIET_BATCHES + 1) * RESTORE_BATCH;
40
41bool RestoreScanReachedDepth(int nSchema,
42 unsigned int nReceiveNext,
43 unsigned int nChangeNext,
44 unsigned int nLegacyNext,
45 int nStopDepth)
46{
47 if (nStopDepth <= 0)
48 return true;
49 unsigned int nDepth = (unsigned int)nStopDepth;
50 if (nSchema == HD_SCHEMA_BIP44)
51 return nReceiveNext >= nDepth &&
52 nChangeNext >= nDepth &&
53 nLegacyNext >= nDepth;
54 return nLegacyNext >= nDepth;
55}
56
57int CmdNewPhrase()
58{
59 AttachTerminal();
60
61 if (HaveHDSeed())
62 {
63 printf("This wallet already has a recovery phrase.\n");
64 printf("Creating another would replace it, and coins on addresses derived from\n");
65 printf("the current one would no longer come back from the phrase you write down.\n");
66 printf("Nothing was changed.\n");
67 return 1;
68 }
69
70 std::string strMnemonic, strError;
71 if (!bitflash::BIP39GenerateMnemonic(strMnemonic, strError))
72 {
73 fprintf(stderr, "Could not generate a phrase: %s\n", strError.c_str());
74 return 1;
75 }
76
77 if (!SetHDSeedFromMnemonic(strMnemonic, strError))
78 {
79 fprintf(stderr, "Could not install the seed: %s\n", strError.c_str());
80 return 1;
81 }
82
83 // Fill the pool from the new seed straight away, so the addresses this
84 // wallet hands out next are ones the phrase can reproduce.
85 TopUpKeyPool();
86
87 printf("\n");
88 printf("Write these twelve words down, in order, on paper.\n");
89 printf("They are the only thing that can rebuild this wallet if the file is lost.\n");
90 printf("Anyone who reads them can spend your coins. They are not stored anywhere,\n");
91 printf("and this is the only time they will be shown.\n");
92 printf("\n");
93 printf(" %s\n", strMnemonic.c_str());
94 printf("\n");
95 printf("Keys already in this wallet are NOT covered by the phrase -- they existed\n");
96 printf("before it did. Keep your file backups as well.\n");
97 fflush(stdout);
98 return 0;
99}
100
101bool RestoreFromPhrase(const std::string& strMnemonic,
102 int nMinDepth,
103 RestoreProgressFn fnProgress,
104 void* pArg,
105 std::string& strErrorRet,
106 int& nRecoveredRet,
107 int& nDerivedRet)
108{
109 strErrorRet.clear();
110 nRecoveredRet = 0;
111 nDerivedRet = 0;
112
113 // Refuse before touching the wallet, not after.
114 //
115 // Without this, a restore on a node that has not synced derives the right
116 // keys, scans a chain that is not there, and reports "no transactions were
117 // found" -- which reads as "your coins are gone" to the one person least
118 // able to argue with it. Measured while testing: a copied data directory
119 // whose Berkeley DB environment failed to load left the node at height 0,
120 // the scan walked one block, and the message said nothing was found across
121 // 600 addresses.
122 if (!CanScanWalletTransactions(strErrorRet))
123 return false;
124
125 size_t nWalletStart = 0;
126 CRITICAL_BLOCK(cs_mapWallet)
127 nWalletStart = mapWallet.size();
128
129 if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet))
130 return false;
131
132 // Derive forward in batches, scanning after each, until a whole batch turns
133 // up nothing. Every derived key is written to the wallet before the scan,
134 // because the scan asks the wallet what belongs to it.
135 int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44
136 ? (int)(nHDReceiveNext + nHDChangeNext)
137 : (int)nHDNext;
138 int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN);
139 int nQuietBatches = 0;
140 while (nTotalDerived < RESTORE_MAX)
141 {
142 // Count wallet transactions, not scan hits.
143 //
144 // ScanForWalletTransactions reports what it added *or updated*, and it
145 // walks the whole chain every time, so a transaction found in the first
146 // batch is reported again by every batch after it. Testing this against
147 // a real payment produced exactly +1 per batch and a loop that ran to
148 // the ceiling, deriving ten thousand addresses to rediscover one coin.
149 size_t nWalletBefore = 0;
150 CRITICAL_BLOCK(cs_mapWallet)
151 nWalletBefore = mapWallet.size();
152
153 std::string strDeriveError;
154 if (nHDKeySchema == HD_SCHEMA_BIP44)
155 {
156 for (int i = 0; i < RESTORE_BATCH; i++)
157 {
158 CKey key;
159 if (!DeriveHDKey(nHDReceiveNext, key, strDeriveError))
160 break;
161 if (!AddKey(key))
162 break;
163 nHDReceiveNext++;
164 nTotalDerived++;
165 }
166 CWalletDB().WriteHDReceiveNext(nHDReceiveNext);
167
168 for (int i = 0; i < RESTORE_BATCH; i++)
169 {
170 CKey key;
171 if (!DeriveHDChangeKey(nHDChangeNext, key, strDeriveError))
172 break;
173 if (!AddKey(key))
174 break;
175 nHDChangeNext++;
176 nTotalDerived++;
177 }
178 CWalletDB().WriteHDChangeNext(nHDChangeNext);
179
180 int nSavedSchema = nHDKeySchema;
181 nHDKeySchema = HD_SCHEMA_LEGACY;
182 for (int i = 0; i < RESTORE_BATCH; i++)
183 {
184 CKey key;
185 if (!DeriveHDKey(nHDNext, key, strDeriveError))
186 break;
187 if (!AddKey(key))
188 break;
189 nHDNext++;
190 nTotalDerived++;
191 }
192 nHDKeySchema = nSavedSchema;
193 CWalletDB().WriteHDNext(nHDNext);
194 }
195 else
196 {
197 for (int i = 0; i < RESTORE_BATCH; i++)
198 {
199 CKey key;
200 if (!DeriveHDKey(nHDNext, key, strDeriveError))
201 break;
202 if (!AddKey(key))
203 break;
204 nHDNext++;
205 nTotalDerived++;
206 }
207 CWalletDB().WriteHDNext(nHDNext);
208 }
209
210 ScanForWalletTransactions(pindexGenesisBlock);
211
212 size_t nWalletAfter = 0;
213 CRITICAL_BLOCK(cs_mapWallet)
214 nWalletAfter = mapWallet.size();
215
216 if (fnProgress)
217 {
218 size_t nRecoveredNow = nWalletAfter > nWalletStart ?
219 nWalletAfter - nWalletStart : 0;
220 fnProgress(pArg, nTotalDerived, (int)nRecoveredNow);
221 }
222
223 // Stopping needs more than one quiet batch, because this wallet digs
224 // gaps in its own derivation. A restore leaves the receive counter at
225 // the depth it scanned, the key pool then derives KEYPOOL_SIZE more,
226 // and a later spend can use change after that -- so the address used
227 // *after* a restore can sit two hundred indices past the last one used
228 // before it, with nothing in between.
229 //
230 // Measured, on a real wallet with real coin: restore, spend once, and
231 // the coins land at indices 201 and 302. Restoring again with the old
232 // one-batch rule stopped at 201 and reported a single transaction. The
233 // money was derived, covered by the phrase, and invisible.
234 if (nWalletAfter == nWalletBefore)
235 nQuietBatches++;
236 else
237 nQuietBatches = 0;
238 if (nQuietBatches >= RESTORE_QUIET_BATCHES &&
239 RestoreScanReachedDepth(nHDKeySchema,
240 nHDReceiveNext,
241 nHDChangeNext,
242 nHDNext,
243 nStopDepth))
244 break;
245 }
246
247 // The pool is rebuilt from where derivation stopped, so the next address
248 // this wallet hands out is one the phrase can reproduce as well.
249 CRITICAL_BLOCK(cs_keyPool)
250 mapKeyPool.clear();
251 TopUpKeyPool();
252
253 size_t nWalletEnd = 0;
254 CRITICAL_BLOCK(cs_mapWallet)
255 nWalletEnd = mapWallet.size();
256 nRecoveredRet = (int)(nWalletEnd > nWalletStart ?
257 nWalletEnd - nWalletStart : 0);
258 nDerivedRet = nTotalDerived;
259 return true;
260}
261
262static void PrintRestoreProgress(void*, int nDerived, int nRecovered)
263{
264 printf(" %d addresses checked, %d transaction(s) recovered\n", nDerived, nRecovered);
265 fflush(stdout);
266}
267
268int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth)
269{
270 AttachTerminal();
271
272 printf("Checking the phrase and the chain.\n");
273 fflush(stdout);
274
275 std::string strError;
276 int nRecovered = 0, nDerived = 0;
277 if (!RestoreFromPhrase(strMnemonic, nMinDepth, PrintRestoreProgress, NULL,
278 strError, nRecovered, nDerived))
279 {
280 fprintf(stderr, "Cannot restore: %s\n", strError.c_str());
281 fprintf(stderr, "Nothing was changed.\n");
282 return 1;
283 }
284
285 printf("\n");
286 if (nRecovered > 0)
287 printf("Restored: %d transaction(s) across %d derived addresses.\n",
288 nRecovered, nDerived);
289 else
290 printf("No transactions were found for the first %d addresses of that phrase.\n"
291 "If you expected coins, check the words and the order.\n", nDerived);
292 printf("Start the node normally to see the balance.\n");
293 fflush(stdout);
294 return 0;
295}
296
297int CmdNewAddress()
298{
299 AttachTerminal();
300
301 std::vector<unsigned char> vchPubKey = GetKeyFromPool();
302 if (vchPubKey.empty())
303 {
304 fprintf(stderr, "Could not get a key from the wallet.\n");
305 return 1;
306 }
307
308 printf("%s\n", PubKeyToAddress(vchPubKey).c_str());
309 if (!HaveHDSeed())
310 printf("(this wallet has no recovery phrase, so only a file backup covers this address)\n");
311 fflush(stdout);
312 return 0;
313}
314
315// Diagnostic: print the addresses a phrase produces, so "the restore found
316// nothing" can be told apart from "that address was never derived".
317int CmdShowDerived(int nCount)
318{
319 AttachTerminal();
320
321 if (!HaveHDSeed())
322 {
323 fprintf(stderr, "This wallet has no recovery phrase installed.\n");
324 return 1;
325 }
326 if (nCount <= 0)
327 nCount = 20;
328
329 std::string strError;
330 for (int i = 0; i < nCount; i++)
331 {
332 CKey key;
333 if (!DeriveHDKey((unsigned int)i, key, strError))
334 {
335 fprintf(stderr, "derivation failed at %d: %s\n", i, strError.c_str());
336 return 1;
337 }
338 printf("%6d %s\n", i, PubKeyToAddress(key.GetPubKey()).c_str());
339 }
340 fflush(stdout);
341 return 0;
342}
343
344int CmdRecoveryAudit()
345{
346 AttachTerminal();
347
348 WalletRecoveryAudit audit = GetWalletRecoveryAudit();
349 int64 nTotal = audit.nRecoverableCredit + audit.nLegacyCredit;
350 int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit;
351
352 printf("Wallet recovery audit\n");
353 printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed");
354 if (audit.fHaveSeed)
355 {
356 printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str());
357 printf(" BIP44 coin type: %u (provisional BITFLASH)\n", audit.nCoinType);
358 printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown);
359 printf(" receive/change counters: %u/%u\n",
360 audit.nReceiveNext, audit.nChangeNext);
361 }
362 if (!audit.fDeriveComplete)
363 printf(" derivation warning: %s\n", audit.strDeriveError.c_str());
364 printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str());
365 printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n",
366 FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx);
367 printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n",
368 FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx);
369 printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str());
370 printf(" phrase-backed immature: %s BTF (%d transaction(s))\n",
371 FormatMoney(audit.nRecoverableImmatureCredit).c_str(),
372 audit.nRecoverableImmatureTx);
373 printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n",
374 FormatMoney(audit.nLegacyImmatureCredit).c_str(),
375 audit.nLegacyImmatureTx);
376
377 if (!audit.fHaveSeed)
378 {
379 printf("\n");
380 printf("This wallet has no recovery phrase. A file backup is the only backup.\n");
381 fflush(stdout);
382 return nTotal + nImmatureTotal > 0 ? 2 : 0;
383 }
384 if (!audit.fDeriveComplete)
385 {
386 printf("\n");
387 printf("Warning: the audit could not derive every known phrase key, so coverage is incomplete.\n");
388 fflush(stdout);
389 return 2;
390 }
391 if (audit.nLegacyCredit > 0 || audit.nLegacyImmatureCredit > 0)
392 {
393 printf("\n");
394 printf("Warning: some coins are on keys the phrase does not reproduce.\n");
395 printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n");
396 fflush(stdout);
397 return 2;
398 }
399
400 printf("\n");
401 if (nTotal + nImmatureTotal > 0)
402 printf("All known wallet balance is covered by the recovery phrase.\n");
403 else
404 printf("No wallet balance found yet.\n");
405 fflush(stdout);
406 return 0;
407}
408
409int CmdEncryptWallet(const std::string& strPassphrase)
410{
411 AttachTerminal();
412
413 string strBackup;
414 string strError;
415 if (!EncryptWallet(strPassphrase, strBackup, strError))
416 {
417 fprintf(stderr, "Cannot encrypt wallet: %s\n", strError.c_str());
418 fprintf(stderr, "Nothing was changed.\n");
419 return 1;
420 }
421
422 printf("Wallet encrypted.\n");
423 printf("The old unencrypted wallet was moved to:\n");
424 printf(" %s\n", strBackup.c_str());
425 printf("That backup still contains private keys in plain text. Move it offline or\n");
426 printf("delete it after you have a safer backup plan.\n");
427 printf("Restart Bitflash before using the wallet again.\n");
428 fflush(stdout);
429
430 // The rewritten wallet.dat is clean; the environment's write-ahead logs are
431 // not, because every plaintext record this wallet ever wrote passed through
432 // them. Closing the environment and dropping the logs is the last step of
433 // encrypting, not an optimisation -- our own backup advice is to copy the
434 // whole directory, and following it otherwise carries the keys along.
435 PurgeDbEnvironmentLogs();
436 return 0;
437}
438
439// Spend, from the command line.
440//
441// SendMoney() has been in this tree since 0.1.0 and only the window ever called
442// it, so a headless node could be paid and could never pay: it held a balance
443// with no way to move it. Found while trying to prove that change from a wallet
444// with a recovery phrase lands on a key the phrase can reproduce -- a test that
445// needs a spend, and there was no way to make one without a screen.
446//
447// Deliberately strict, because this moves money and there is nobody to click
448// "are you sure": the address must decode, the amount must parse and be
449// positive, and anything else refuses before touching the wallet.
450int CmdSendTo(const std::string& strArg)
451{
452 AttachTerminal();
453
454 if (IsWalletLocked())
455 {
456 fprintf(stderr, "Wallet is encrypted and locked. Start with /walletpassphrase or /walletpassphrase=@FILE to spend.\n");
457 return 1;
458 }
459
460 std::string::size_type comma = strArg.rfind(',');
461 if (comma == std::string::npos)
462 {
463 fprintf(stderr, "Usage: -sendto=ADDRESS,AMOUNT (for example -sendto=B7kQ...,1.5)\n");
464 return 1;
465 }
466 std::string strAddr = strArg.substr(0, comma);
467 std::string strAmount = strArg.substr(comma + 1);
468
469 // Trim, so a quoted argument with stray spaces does not silently become an
470 // invalid address and send nothing.
471 while (!strAddr.empty() && isspace((unsigned char)strAddr[0])) strAddr.erase(0, 1);
472 while (!strAddr.empty() && isspace((unsigned char)strAddr[strAddr.size()-1])) strAddr.erase(strAddr.size()-1);
473 while (!strAmount.empty() && isspace((unsigned char)strAmount[0])) strAmount.erase(0, 1);
474 while (!strAmount.empty() && isspace((unsigned char)strAmount[strAmount.size()-1])) strAmount.erase(strAmount.size()-1);
475
476 uint160 hash160;
477 if (!AddressToHash160(strAddr, hash160))
478 {
479 fprintf(stderr, "Not a valid address: %s\n", strAddr.c_str());
480 return 1;
481 }
482
483 int64 nValue = 0;
484 if (!ParseMoney(strAmount.c_str(), nValue) || nValue <= 0)
485 {
486 fprintf(stderr, "Not a valid amount: %s\n", strAmount.c_str());
487 return 1;
488 }
489
490 std::string strWhy;
491 if (!CanScanWalletTransactions(strWhy))
492 {
493 // Spending needs the chain: without it the wallet cannot tell which of
494 // its outputs are still unspent, and a transaction built on that guess
495 // is one the network will reject.
496 fprintf(stderr, "The block chain is not loaded, so this wallet cannot tell "
497 "which coins it still has. Start the node and let it sync first.\n");
498 return 1;
499 }
500
501 CScript scriptPubKey;
502 scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;
503
504 CWalletTx wtx;
505 if (!SendMoney(scriptPubKey, nValue, wtx))
506 {
507 fprintf(stderr, "The transaction was not created. Usually that means the "
508 "balance is too low once the fee is counted.\n");
509 return 1;
510 }
511
512 printf("Sent %s to %s\n", FormatMoney(nValue).c_str(), strAddr.c_str());
513 printf(" transaction %s\n", wtx.GetHash().ToString().c_str());
514 printf(" it needs to be relayed and mined before the other side sees it.\n");
515 fflush(stdout);
516 return 0;
517}