upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/db.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-30 11:27:49 -0300
committerGitHub <noreply@github.com>2026-07-30 11:27:49 -0300
commit99f7c1dfebb1a15dc6f77a338a8b0aaa3962564b (patch)
tree1e5a5bae21f75600d945efb3f56de68fdd33bce1 /src/db.cpp
parentde884e493145bd5cbd1d7a049968edcd110391af (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.
Diffstat (limited to 'src/db.cpp')
-rw-r--r--src/db.cpp134
1 files changed, 131 insertions, 3 deletions
diff --git a/src/db.cpp b/src/db.cpp
index d43e4ef..fc5f180 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -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.
650bool 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}