diff options
| author | Bitflash <btf01@proton.me> | 2026-07-30 11:27:49 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-30 11:27:49 -0300 |
| commit | 99f7c1dfebb1a15dc6f77a338a8b0aaa3962564b (patch) | |
| tree | 1e5a5bae21f75600d945efb3f56de68fdd33bce1 | |
| parent | de884e493145bd5cbd1d7a049968edcd110391af (diff) | |
wallet: make wallet.dat a file you can actually back up (#46)
* wallet: make wallet.dat a file you can actually back up
Issue #40: a wallet.dat copied out on its own will not open
anywhere else. The file is intact -- same bytes, same keys --
and the node refuses it with "Db::put: Invalid argument",
thrown from a path that aborted the process. The reporter
believes they lost funds to this, having saved the one file
anybody would think to save.
The chain, from the bottom:
_endthread() is pthread_exit() on POSIX, and glibc implements
that by throwing abi::__forced_unwind through the stack. That
is not a real exception; catching it without rethrowing is a
fatal error by contract. CATCH_PRINT_EXCEPTION ended in a bare
catch(...), so every Linux shutdown ended:
UNKNOWN EXCEPTION bitflash-node in ThreadSocketHandler()
FATAL: exception not rethrown
An abort, mid-shutdown, so nothing after StopNode() ran.
Which mattered because Berkeley DB stamps every page with a
log sequence number belonging to the environment that wrote
it. Carried elsewhere, those numbers point at logs that do not
exist and BDB refuses to write. DBFlush(true) calls lsn_reset,
which is precisely the cure -- and DBFlush has been declared
since 2009 and called from nowhere. Even had it been called,
the abort stood in front of it.
So the wallet was never released from its directory, and the
one file users copy was the one file that could not travel.
Three fixes, each covering a different case:
- Rethrow abi::__forced_unwind, and call DBFlush(true) on
both shutdown paths. Shutdown completes, and wallet.dat is
left portable by construction.
- Adopt orphaned files on open: a freshly created database
environment beside pre-existing .dat files means they came
from elsewhere, so reset their sequence numbers. This is
what recovers wallets already stranded by older builds --
including, if they still hold the file, the reporter's.
- BackupWallet(), reachable as /backupwallet=FILE, which
checkpoints, copies, and clears the copy's sequence
numbers. Bitcoin 0.1.0 had a backup command; this tree
lost it in the wxWidgets removal and has shipped with no
way to take a backup since.
DBFlush's own txn_checkpoint and lsn_reset are wrapped too --
they throw, and throwing on the way out is how this started.
Verified on Linux and Windows: shutdown exits 0 rather than
134, DBFlush runs, and a lone wallet.dat opens in an empty
directory with no exceptions and byte-identical key records
(defaultkey, key, name, setting, version) before and after.
Not addressed here, and the larger half of #40: keys are
generated as they are needed rather than derived from a seed,
so any backup is a snapshot and coins paid to addresses
created after it are not in it. /backupwallet says so on every
run. The real answer is deterministic derivation, which is a
wallet format change and its own piece of work.
* README: document what a wallet backup actually requires
The PR description claimed this section existed and it did not.
Covers the two things that cost money and are not obvious: a lone
wallet.dat is refused elsewhere because Berkeley DB ties it to the
database/ directory beside it, and every backup is a snapshot
because keys are made on demand rather than derived from a seed.
| -rw-r--r-- | README.md | 31 | ||||
| -rw-r--r-- | src/db.cpp | 134 | ||||
| -rw-r--r-- | src/db.h | 6 | ||||
| -rw-r--r-- | src/main_gui.cpp | 32 | ||||
| -rw-r--r-- | src/util.h | 32 |
5 files changed, 232 insertions, 3 deletions
| @@ -69,6 +69,37 @@ backup — it holds your keys. | |||
| 69 | 69 | ||
| 70 | --- | 70 | --- |
| 71 | 71 | ||
| 72 | ## Backing up your wallet | ||
| 73 | |||
| 74 | Two things about this wallet will cost you money if you do not know them. Both | ||
| 75 | are consequences of the 0.1.0 wallet format, and neither is obvious. | ||
| 76 | |||
| 77 | **Copying `wallet.dat` on its own is not a backup.** Berkeley DB ties the file | ||
| 78 | to the environment in the `database/` subdirectory beside it, so a lone | ||
| 79 | `wallet.dat` will not open elsewhere — the keys are all still in there, and the | ||
| 80 | file is refused anyway. Use the built-in command, which writes a copy that | ||
| 81 | stands on its own: | ||
| 82 | |||
| 83 | ``` | ||
| 84 | bitflash -backupwallet=/path/to/wallet-backup.dat | ||
| 85 | ``` | ||
| 86 | |||
| 87 | It loads the wallet, writes the copy, and exits without starting the node. If | ||
| 88 | you would rather copy by hand, shut the node down first and take the **whole** | ||
| 89 | data directory, not just `wallet.dat`. | ||
| 90 | |||
| 91 | **Every backup is a snapshot, so repeat it.** Keys are generated when they are | ||
| 92 | needed rather than derived from a seed, so a backup contains only the keys that | ||
| 93 | existed the moment it was taken. Mine a block or receive to a new address | ||
| 94 | afterwards, and those coins sit on-chain at an address whose private key is in | ||
| 95 | no backup you hold. Back up again whenever you use a new address. | ||
| 96 | |||
| 97 | The second point is the pre-BIP32 problem that deterministic wallets were | ||
| 98 | invented to solve; see [#47](https://github.com/Bitflash-sh/bitflash/issues/47). | ||
| 99 | Until that lands, repetition is the only defence. | ||
| 100 | |||
| 101 | --- | ||
| 102 | |||
| 72 | ## Mining | 103 | ## Mining |
| 73 | 104 | ||
| 74 | Open **Options** from the menu bar. Under Mining Mode: | 105 | Open **Options** from the menu bar. Under Mining Mode: |
| @@ -59,7 +59,12 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | |||
| 59 | { | 59 | { |
| 60 | string strAppDir = GetAppDir(); | 60 | string strAppDir = GetAppDir(); |
| 61 | string strLogDir = strAppDir + "/database"; | 61 | string strLogDir = strAppDir + "/database"; |
| 62 | _mkdir(strLogDir.c_str()); | 62 | |
| 63 | // _mkdir returns 0 only when it actually created the directory, so | ||
| 64 | // this is also the test for "this environment did not exist until | ||
| 65 | // now". See the recovery block after dbenv.open() for why we care. | ||
| 66 | bool fFreshEnv = (_mkdir(strLogDir.c_str()) == 0); | ||
| 67 | |||
| 63 | printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); | 68 | printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); |
| 64 | 69 | ||
| 65 | dbenv.set_lg_dir(strLogDir.c_str()); | 70 | dbenv.set_lg_dir(strLogDir.c_str()); |
| @@ -81,6 +86,41 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | |||
| 81 | if (ret > 0) | 86 | if (ret > 0) |
| 82 | throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); | 87 | throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); |
| 83 | fDbEnvInit = true; | 88 | fDbEnvInit = true; |
| 89 | |||
| 90 | // A Berkeley DB file records, in every page header, a log sequence | ||
| 91 | // number belonging to the environment that wrote it. Carried into a | ||
| 92 | // different environment those numbers refer to logs that do not | ||
| 93 | // exist, and BDB refuses to write: "Db::put: Invalid argument", | ||
| 94 | // thrown from a path that used to abort the process. | ||
| 95 | // | ||
| 96 | // That is what happens when someone backs up wallet.dat on its own. | ||
| 97 | // It is the obviously important file, it is the only one people | ||
| 98 | // copy, and the environment it is welded to lives in a database/ | ||
| 99 | // subdirectory nobody has reason to suspect. The file is intact -- | ||
| 100 | // same bytes, same keys -- and the wallet still will not open. | ||
| 101 | // Issue #40 reports exactly this, after the fact. | ||
| 102 | // | ||
| 103 | // lsn_reset() exists for precisely this migration, so do it for | ||
| 104 | // them. A fresh environment beside pre-existing .dat files means | ||
| 105 | // those files came from somewhere else; in the normal case the | ||
| 106 | // environment is already there and none of this runs. | ||
| 107 | if (fFreshEnv) | ||
| 108 | { | ||
| 109 | static const char* pszBdbFiles[] = { "wallet.dat", "blkindex.dat" }; | ||
| 110 | for (int i = 0; i < (int)(sizeof(pszBdbFiles)/sizeof(pszBdbFiles[0])); i++) | ||
| 111 | { | ||
| 112 | const char* pszName = pszBdbFiles[i]; | ||
| 113 | if (!FileExists((strAppDir + "/" + pszName).c_str())) | ||
| 114 | continue; | ||
| 115 | try | ||
| 116 | { | ||
| 117 | if (dbenv.lsn_reset(pszName, 0) == 0) | ||
| 118 | printf("CDB() : adopted %s from another environment " | ||
| 119 | "(log sequence numbers reset)\n", pszName); | ||
| 120 | } | ||
| 121 | catch (...) { } // best effort; the open below reports real trouble | ||
| 122 | } | ||
| 123 | } | ||
| 84 | } | 124 | } |
| 85 | 125 | ||
| 86 | strFile = pszFile; | 126 | strFile = pszFile; |
| @@ -137,7 +177,14 @@ void DBFlush(bool fShutdown) | |||
| 137 | printf("DBFlush(%s)\n", fShutdown ? "true" : "false"); | 177 | printf("DBFlush(%s)\n", fShutdown ? "true" : "false"); |
| 138 | CRITICAL_BLOCK(cs_db) | 178 | CRITICAL_BLOCK(cs_db) |
| 139 | { | 179 | { |
| 140 | dbenv.txn_checkpoint(0, 0, 0); | 180 | // This runs on the way out. Anything that throws here and is not caught |
| 181 | // takes the process down before the wallet has been put down cleanly, | ||
| 182 | // which is the difference between a portable wallet.dat and one welded | ||
| 183 | // to this directory -- the lsn_reset just below is what frees it. | ||
| 184 | try { dbenv.txn_checkpoint(0, 0, 0); } | ||
| 185 | catch (const std::exception& e) | ||
| 186 | { printf("DBFlush() : checkpoint failed: %s\n", e.what()); } | ||
| 187 | |||
| 141 | map<string, int>::iterator mi = mapFileUseCount.begin(); | 188 | map<string, int>::iterator mi = mapFileUseCount.begin(); |
| 142 | while (mi != mapFileUseCount.end()) | 189 | while (mi != mapFileUseCount.end()) |
| 143 | { | 190 | { |
| @@ -145,7 +192,9 @@ void DBFlush(bool fShutdown) | |||
| 145 | int nRefCount = (*mi).second; | 192 | int nRefCount = (*mi).second; |
| 146 | if (nRefCount == 0) | 193 | if (nRefCount == 0) |
| 147 | { | 194 | { |
| 148 | dbenv.lsn_reset(strFile.c_str(), 0); | 195 | try { dbenv.lsn_reset(strFile.c_str(), 0); } |
| 196 | catch (const std::exception& e) | ||
| 197 | { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); } | ||
| 149 | mapFileUseCount.erase(mi++); | 198 | mapFileUseCount.erase(mi++); |
| 150 | } | 199 | } |
| 151 | else | 200 | else |
| @@ -580,3 +629,82 @@ bool LoadWallet() | |||
| 580 | 629 | ||
| 581 | return true; | 630 | return true; |
| 582 | } | 631 | } |
| 632 | |||
| 633 | |||
| 634 | // Write a copy of wallet.dat that can be opened anywhere. | ||
| 635 | // | ||
| 636 | // A plain file copy is not a backup here. Berkeley DB stamps every page with a | ||
| 637 | // log sequence number tied to the environment that wrote it, so the copy only | ||
| 638 | // works beside the database/ directory it grew up with -- which is why backing | ||
| 639 | // up "just wallet.dat", the one file anybody would think to save, produces | ||
| 640 | // something that will not open. See the recovery block in CDB::CDB and #40. | ||
| 641 | // | ||
| 642 | // So: flush outstanding writes, copy, then clear the copy's log sequence | ||
| 643 | // numbers. What lands on disk is a wallet.dat that stands on its own. | ||
| 644 | // | ||
| 645 | // This is still a point-in-time snapshot. Keys are generated as they are | ||
| 646 | // needed, not derived from a seed, so coins paid to an address created after | ||
| 647 | // this file was written are not spendable from it. Backing up once is not | ||
| 648 | // enough, and that is a property of the wallet format rather than of this | ||
| 649 | // function. | ||
| 650 | bool BackupWallet(const string& strDest) | ||
| 651 | { | ||
| 652 | if (strDest.empty()) | ||
| 653 | return error("BackupWallet() : no destination given\n"); | ||
| 654 | |||
| 655 | string strSrc = GetAppDir() + "/wallet.dat"; | ||
| 656 | if (!FileExists(strSrc.c_str())) | ||
| 657 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); | ||
| 658 | |||
| 659 | CRITICAL_BLOCK(cs_db) | ||
| 660 | { | ||
| 661 | // Committing first, so the copy is not missing the newest records. | ||
| 662 | // Both calls can throw, and a failed backup must not take the node | ||
| 663 | // down with it. | ||
| 664 | try { dbenv.txn_checkpoint(0, 0, 0); } | ||
| 665 | catch (const std::exception& e) | ||
| 666 | { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); } | ||
| 667 | |||
| 668 | try | ||
| 669 | { | ||
| 670 | FILE* pfIn = fopen(strSrc.c_str(), "rb"); | ||
| 671 | if (!pfIn) | ||
| 672 | return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); | ||
| 673 | FILE* pfOut = fopen(strDest.c_str(), "wb"); | ||
| 674 | if (!pfOut) | ||
| 675 | { | ||
| 676 | fclose(pfIn); | ||
| 677 | return error("BackupWallet() : cannot write %s\n", strDest.c_str()); | ||
| 678 | } | ||
| 679 | |||
| 680 | char buf[65536]; | ||
| 681 | size_t n; | ||
| 682 | bool fOk = true; | ||
| 683 | while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0) | ||
| 684 | if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; } | ||
| 685 | if (ferror(pfIn)) | ||
| 686 | fOk = false; | ||
| 687 | fclose(pfIn); | ||
| 688 | // Flushed and closed before lsn_reset touches it. | ||
| 689 | if (fclose(pfOut) != 0) | ||
| 690 | fOk = false; | ||
| 691 | |||
| 692 | if (!fOk) | ||
| 693 | { | ||
| 694 | remove(strDest.c_str()); // half a wallet is worse than none | ||
| 695 | return error("BackupWallet() : copy to %s failed\n", strDest.c_str()); | ||
| 696 | } | ||
| 697 | |||
| 698 | // Without this the copy is welded to this node's database/ dir. | ||
| 699 | int ret = dbenv.lsn_reset(strDest.c_str(), 0); | ||
| 700 | if (ret != 0) | ||
| 701 | printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " | ||
| 702 | "may only open beside this node's database/ directory\n", ret); | ||
| 703 | } | ||
| 704 | catch (const std::exception& e) | ||
| 705 | { return error("BackupWallet() : %s\n", e.what()); } | ||
| 706 | } | ||
| 707 | |||
| 708 | printf("BackupWallet() : wrote %s\n", strDest.c_str()); | ||
| 709 | return true; | ||
| 710 | } | ||
| @@ -403,6 +403,12 @@ public: | |||
| 403 | 403 | ||
| 404 | bool LoadWallet(); | 404 | bool LoadWallet(); |
| 405 | 405 | ||
| 406 | // Writes a wallet.dat that opens on its own, anywhere -- see the definition in | ||
| 407 | // db.cpp for why a plain file copy does not. Still a point-in-time snapshot: | ||
| 408 | // keys are made as needed rather than derived from a seed, so coins paid to an | ||
| 409 | // address created after this call are not spendable from the file it writes. | ||
| 410 | bool BackupWallet(const string& strDest); | ||
| 411 | |||
| 406 | inline bool SetAddressBookName(const string& strAddress, const string& strName) | 412 | inline bool SetAddressBookName(const string& strAddress, const string& strName) |
| 407 | { | 413 | { |
| 408 | return CWalletDB().WriteName(strAddress, strName); | 414 | return CWalletDB().WriteName(strAddress, strName); |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index c64dca9..976cd42 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -67,6 +67,10 @@ static void PrintUsage() | |||
| 67 | printf(" /port=N (P2P listen port, default 8433)\n"); | 67 | printf(" /port=N (P2P listen port, default 8433)\n"); |
| 68 | printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); | 68 | printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); |
| 69 | printf("\n"); | 69 | printf("\n"); |
| 70 | printf("Wallet:\n"); | ||
| 71 | printf(" /backupwallet=FILE (write a wallet.dat that opens on its own,\n"); | ||
| 72 | printf(" then exit; run it again after new addresses)\n"); | ||
| 73 | printf("\n"); | ||
| 70 | printf("Each option also accepts '-' instead of '/'.\n"); | 74 | printf("Each option also accepts '-' instead of '/'.\n"); |
| 71 | } | 75 | } |
| 72 | 76 | ||
| @@ -219,6 +223,28 @@ int main(int argc, char* argv[]) | |||
| 219 | return 1; | 223 | return 1; |
| 220 | } | 224 | } |
| 221 | printf("Height=%d\n", nBestHeight); | 225 | printf("Height=%d\n", nBestHeight); |
| 226 | |||
| 227 | // Before the node opens sockets or touches anything: the wallet is loaded, | ||
| 228 | // which is all a backup needs, and finishing here means the copy is taken | ||
| 229 | // from a quiet directory rather than from under a running node. | ||
| 230 | string strBackup = argval2(argc, argv, "/backupwallet", "-backupwallet"); | ||
| 231 | if (!strBackup.empty()) | ||
| 232 | { | ||
| 233 | bool fOk = BackupWallet(strBackup); | ||
| 234 | if (fOk) | ||
| 235 | { | ||
| 236 | printf("Backed up to %s\n", strBackup.c_str()); | ||
| 237 | printf("This file opens on its own -- it does not need the database/ " | ||
| 238 | "directory beside it.\n"); | ||
| 239 | printf("It is a snapshot: coins paid to addresses created after now are " | ||
| 240 | "not in it, so back up again whenever you receive to a new address.\n"); | ||
| 241 | } | ||
| 242 | else | ||
| 243 | fprintf(stderr, "Backup failed -- nothing was written.\n"); | ||
| 244 | DBFlush(true); | ||
| 245 | return fOk ? 0 : 1; | ||
| 246 | } | ||
| 247 | |||
| 222 | ReacceptWalletTransactions(); | 248 | ReacceptWalletTransactions(); |
| 223 | 249 | ||
| 224 | if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } | 250 | if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } |
| @@ -249,6 +275,11 @@ int main(int argc, char* argv[]) | |||
| 249 | printf("Running headless. Ctrl-C to stop.\n"); | 275 | printf("Running headless. Ctrl-C to stop.\n"); |
| 250 | while (!fShutdown) Sleep(500); | 276 | while (!fShutdown) Sleep(500); |
| 251 | StopNode(); | 277 | StopNode(); |
| 278 | // Checkpoints, releases wallet.dat from this directory's Berkeley DB | ||
| 279 | // environment, and closes it. Declared since 2009 and never once | ||
| 280 | // called here, which is why a wallet.dat copied elsewhere would not | ||
| 281 | // open -- issue #40. Not optional. | ||
| 282 | DBFlush(true); | ||
| 252 | return 0; | 283 | return 0; |
| 253 | } | 284 | } |
| 254 | 285 | ||
| @@ -258,6 +289,7 @@ int main(int argc, char* argv[]) | |||
| 258 | int ret = RunGUI(argc, argv); | 289 | int ret = RunGUI(argc, argv); |
| 259 | fShutdown = true; | 290 | fShutdown = true; |
| 260 | StopNode(); | 291 | StopNode(); |
| 292 | DBFlush(true); | ||
| 261 | return ret; | 293 | return ret; |
| 262 | #endif | 294 | #endif |
| 263 | } | 295 | } |
| @@ -3,6 +3,11 @@ | |||
| 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. | 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | // For abi::__forced_unwind, which CATCH_PRINT_EXCEPTION below must let past. | ||
| 7 | #ifndef _WIN32 | ||
| 8 | #include <cxxabi.h> | ||
| 9 | #endif | ||
| 10 | |||
| 6 | #if defined(_MSC_VER) || defined(__BORLANDC__) | 11 | #if defined(_MSC_VER) || defined(__BORLANDC__) |
| 7 | typedef __int64 int64; | 12 | typedef __int64 int64; |
| 8 | typedef unsigned __int64 uint64; | 13 | typedef unsigned __int64 uint64; |
| @@ -419,7 +424,34 @@ inline void heapchk() | |||
| 419 | } \ | 424 | } \ |
| 420 | } | 425 | } |
| 421 | 426 | ||
| 427 | // The forced-unwind arm has to come first, and it has to rethrow. | ||
| 428 | // | ||
| 429 | // On POSIX, _endthread() is pthread_exit(), and glibc implements that by | ||
| 430 | // throwing abi::__forced_unwind through the stack. It is not a real exception | ||
| 431 | // and catching it without rethrowing is a fatal error by contract: glibc | ||
| 432 | // notices and calls abort(). | ||
| 433 | // | ||
| 434 | // The bare catch(...) below did exactly that. Every shutdown on Linux died with | ||
| 435 | // | ||
| 436 | // UNKNOWN EXCEPTION bitflash-node in ThreadSocketHandler() | ||
| 437 | // FATAL: exception not rethrown | ||
| 438 | // | ||
| 439 | // which looked like nothing worse than an ugly exit, and was not: the process | ||
| 440 | // aborted partway through shutting down, so nothing after StopNode() ran. See | ||
| 441 | // main_gui.cpp, where DBFlush(true) is now called -- it could never have | ||
| 442 | // worked while this abort stood in front of it. Windows is unaffected; there | ||
| 443 | // _endthread() is the CRT's and unwinds nothing. | ||
| 444 | #ifdef _WIN32 | ||
| 445 | #define CATCH_FORCED_UNWIND | ||
| 446 | #else | ||
| 447 | #define CATCH_FORCED_UNWIND \ | ||
| 448 | catch (abi::__forced_unwind&) { \ | ||
| 449 | throw; \ | ||
| 450 | } | ||
| 451 | #endif | ||
| 452 | |||
| 422 | #define CATCH_PRINT_EXCEPTION(pszFn) \ | 453 | #define CATCH_PRINT_EXCEPTION(pszFn) \ |
| 454 | CATCH_FORCED_UNWIND \ | ||
| 423 | catch (std::exception& e) { \ | 455 | catch (std::exception& e) { \ |
| 424 | PrintException(&e, (pszFn)); \ | 456 | PrintException(&e, (pszFn)); \ |
| 425 | } catch (...) { \ | 457 | } catch (...) { \ |