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 <admin@bitflash.network>2026-08-18 15:48:17 -0300
committerBitflash <admin@bitflash.network>2026-08-18 15:48:17 -0300
commitf3fb7ce6759ac98a7cc39d586fc2824982c74acd (patch)
tree23e39a26b340324a7a33a115aded3b70b5332816 /src/db.cpp
parentbeb5c1435a0d85b7b24d4d371933758ebed90907 (diff)
parent84a60275569dca5d1fb12db38ce184caf7487ed6 (diff)
Merge pull request 'wallet: complete SQLite backend activation (encrypt, atomicity, one-shot commands)' (#61) from wallet-sqlite-encrypt into main
Diffstat (limited to 'src/db.cpp')
-rw-r--r--src/db.cpp356
1 files changed, 353 insertions, 3 deletions
diff --git a/src/db.cpp b/src/db.cpp
index 771a4ba..aafc0f9 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -164,6 +164,72 @@ bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey)
164 return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); 164 return pWalletSQLiteRuntime->EraseRecord(vchKey, strError);
165} 165}
166 166
167// Transaction bracket for the SQLite runtime. When the SQLite backend is not
168// active these are no-ops that succeed, so a caller can wrap a group of related
169// writes and Berkeley DB keeps its existing per-write behaviour unchanged while
170// SQLite gets all-or-nothing.
171bool WalletSQLiteRuntimeBeginTxn()
172{
173 if (!pWalletSQLiteRuntime)
174 return true;
175 string strError;
176 if (pWalletSQLiteRuntime->BeginTransaction(strError))
177 return true;
178 printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str());
179 return false;
180}
181
182bool WalletSQLiteRuntimeCommitTxn()
183{
184 if (!pWalletSQLiteRuntime)
185 return true;
186 string strError;
187 if (pWalletSQLiteRuntime->CommitTransaction(strError))
188 return true;
189 printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str());
190 return false;
191}
192
193void WalletSQLiteRuntimeRollbackTxn()
194{
195 if (!pWalletSQLiteRuntime)
196 return;
197 string strError;
198 pWalletSQLiteRuntime->RollbackTransaction(strError);
199}
200
201// Open the SQLite wallet read-only as the active runtime. Used by the one-shot
202// storage diagnostics, which run before the normal wallet load and must be able
203// to inspect a wallet that the loader would refuse -- so this opens the file for
204// scanning without loading or validating its records.
205bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet)
206{
207 strErrorRet.clear();
208 if (!FileExists(strPath.c_str()))
209 {
210 strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str());
211 return false;
212 }
213
214 WalletSQLiteRuntimeClose();
215 std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite());
216 if (!db->OpenReadOnly(strPath, strErrorRet))
217 return false;
218
219 pWalletSQLiteRuntime = db.release();
220 return true;
221}
222
223bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet)
224{
225 if (!pWalletSQLiteRuntime)
226 {
227 strErrorRet = "SQLite wallet backend is not active";
228 return false;
229 }
230 return pWalletSQLiteRuntime->Checkpoint(strErrorRet);
231}
232
167 233
168CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) 234CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
169{ 235{
@@ -1320,6 +1386,16 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
1320 1386
1321bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) 1387bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet)
1322{ 1388{
1389 // Under the SQLite backend there is no Berkeley DB cursor to walk; scan the
1390 // active SQLite store instead. This is the single choke point every "read
1391 // every wallet record" caller goes through (storage audit and check among
1392 // them), so routing it here makes those honour -walletbackend=sqlite.
1393 if (WalletSQLiteRuntimeActive())
1394 {
1395 strErrorRet.clear();
1396 return pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet);
1397 }
1398
1323 class CWalletScanDB : public CWalletDB 1399 class CWalletScanDB : public CWalletDB
1324 { 1400 {
1325 public: 1401 public:
@@ -1550,11 +1626,81 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath)
1550// this file was written are not spendable from it. Backing up once is not 1626// this file was written are not spendable from it. Backing up once is not
1551// enough, and that is a property of the wallet format rather than of this 1627// enough, and that is a property of the wallet format rather than of this
1552// function. 1628// function.
1629// Back up the active SQLite wallet. The store is checkpointed so the WAL is
1630// folded into the main file, then the single self-contained wallet.sqlite is
1631// copied through a temp file and renamed into place (same temp/old/rename dance
1632// as the Berkeley DB path, for Windows' non-overwriting rename). No lsn_reset:
1633// a checkpointed SQLite file needs nothing beside it to open.
1634static bool BackupWalletSQLite(const string& strDest)
1635{
1636 string strSrc = GetAppDir() + "/wallet.sqlite";
1637 if (!FileExists(strSrc.c_str()))
1638 return error("BackupWallet() : %s does not exist\n", strSrc.c_str());
1639
1640 string strCheckpointError;
1641 if (!WalletSQLiteRuntimeCheckpoint(strCheckpointError))
1642 return error("BackupWallet() : checkpoint failed: %s\n", strCheckpointError.c_str());
1643
1644 string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime());
1645 for (int i = 1; FileExists(strTemp.c_str()); i++)
1646 strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i);
1647 string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime());
1648 for (int i = 1; FileExists(strOld.c_str()); i++)
1649 strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i);
1650
1651 FILE* pfIn = fopen(strSrc.c_str(), "rb");
1652 if (!pfIn)
1653 return error("BackupWallet() : cannot read %s\n", strSrc.c_str());
1654 FILE* pfOut = fopen(strTemp.c_str(), "wb");
1655 if (!pfOut)
1656 {
1657 fclose(pfIn);
1658 return error("BackupWallet() : cannot write %s\n", strTemp.c_str());
1659 }
1660
1661 char buf[65536];
1662 size_t n;
1663 bool fOk = true;
1664 while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0)
1665 if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; }
1666 if (ferror(pfIn))
1667 fOk = false;
1668 fclose(pfIn);
1669 if (fclose(pfOut) != 0)
1670 fOk = false;
1671 if (!fOk)
1672 {
1673 remove(strTemp.c_str()); // half a wallet is worse than none
1674 return error("BackupWallet() : copy to %s failed\n", strTemp.c_str());
1675 }
1676
1677 bool fHadOldDest = FileExists(strDest.c_str());
1678 if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0)
1679 {
1680 remove(strTemp.c_str());
1681 return error("BackupWallet() : could not move old %s aside\n", strDest.c_str());
1682 }
1683 if (rename(strTemp.c_str(), strDest.c_str()) != 0)
1684 {
1685 remove(strTemp.c_str());
1686 if (fHadOldDest)
1687 rename(strOld.c_str(), strDest.c_str());
1688 return error("BackupWallet() : could not install %s\n", strDest.c_str());
1689 }
1690 if (fHadOldDest)
1691 remove(strOld.c_str());
1692 return true;
1693}
1694
1553bool BackupWallet(const string& strDest) 1695bool BackupWallet(const string& strDest)
1554{ 1696{
1555 if (strDest.empty()) 1697 if (strDest.empty())
1556 return error("BackupWallet() : no destination given\n"); 1698 return error("BackupWallet() : no destination given\n");
1557 1699
1700 // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat.
1701 if (WalletSQLiteRuntimeActive())
1702 return BackupWalletSQLite(strDest);
1703
1558 string strSrc = GetAppDir() + "/wallet.dat"; 1704 string strSrc = GetAppDir() + "/wallet.dat";
1559 if (!FileExists(strSrc.c_str())) 1705 if (!FileExists(strSrc.c_str()))
1560 return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); 1706 return error("BackupWallet() : %s does not exist\n", strSrc.c_str());
@@ -1974,9 +2120,9 @@ static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo,
1974 return ScanWalletRecords(visitor, strErrorRet); 2120 return ScanWalletRecords(visitor, strErrorRet);
1975} 2121}
1976 2122
1977static string WalletEncryptBackupPath() 2123static string WalletEncryptBackupPathFor(const char* pszBaseName)
1978{ 2124{
1979 string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; 2125 string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt.";
1980 string strPath = strBase + i64tostr(GetTime()) + ".bak"; 2126 string strPath = strBase + i64tostr(GetTime()) + ".bak";
1981 for (int i = 1; FileExists(strPath.c_str()); i++) 2127 for (int i = 1; FileExists(strPath.c_str()); i++)
1982 strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; 2128 strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak";
@@ -2006,6 +2152,192 @@ static void SecureClearBytes(vector<unsigned char>& vch)
2006 vch.clear(); 2152 vch.clear();
2007} 2153}
2008 2154
2155// The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list,
2156// same byte-for-byte copy, but the destination is a CWalletDBSQLite record
2157// store rather than a Berkeley DB file.
2158class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor
2159{
2160public:
2161 CWalletDBSQLite& dbTo;
2162 int& nCopied;
2163
2164 CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn)
2165 : dbTo(dbToIn), nCopied(nCopiedIn) { }
2166
2167 bool VisitWalletRecord(const CDataStream& ssKey,
2168 const CDataStream& ssValue,
2169 string& strErrorRet)
2170 {
2171 string strType;
2172 try
2173 {
2174 CDataStream ssType = ssKey;
2175 ssType >> strType;
2176 }
2177 catch (...)
2178 {
2179 strErrorRet = "SQLite wallet has a record whose key type cannot be read";
2180 return false;
2181 }
2182
2183 if (IsWalletEncryptionPrivateRecord(strType))
2184 return true;
2185
2186 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
2187 vector<unsigned char> vchValue(ssValue.begin(), ssValue.end());
2188 bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true);
2189 if (!vchKey.empty())
2190 memset(&vchKey[0], 0, vchKey.size());
2191 if (!vchValue.empty())
2192 memset(&vchValue[0], 0, vchValue.size());
2193 if (!fOk)
2194 {
2195 strErrorRet = strprintf("could not copy wallet record '%s': %s",
2196 strType.c_str(), strErrorRet.c_str());
2197 return false;
2198 }
2199 nCopied++;
2200 return true;
2201 }
2202};
2203
2204// Persist an encrypted wallet under the SQLite backend. Everything the caller
2205// computed -- the master key, the re-encrypted private keys, the fresh crypted
2206// key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built
2207// in one transaction, which is then swapped in for the plaintext original.
2208//
2209// A fresh file rather than an in-place rewrite for two reasons: it keeps the
2210// whole change atomic (a crash before the rename leaves the old wallet whole),
2211// and it guarantees no plaintext key survives in a freed page the way an
2212// in-place DELETE would leave behind until a VACUUM.
2213static bool EncryptWalletPersistSQLite(
2214 const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vCryptedKeys,
2215 const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vNewPoolKeys,
2216 const vector<pair<int64, vector<unsigned char> > >& vNewPool,
2217 const CWalletMasterKey& kMasterKey,
2218 const vector<unsigned char>& vchCryptedHDMasterNew,
2219 const vector<unsigned char>& vchCryptedHDChainCodeNew,
2220 unsigned int nHDNextNew,
2221 int& nCopiedRet,
2222 string& strBackupRet,
2223 string& strErrorRet)
2224{
2225 nCopiedRet = 0;
2226 if (!pWalletSQLiteRuntime)
2227 {
2228 strErrorRet = "SQLite wallet backend is not active";
2229 return false;
2230 }
2231
2232 string strWalletPath = GetAppDir() + "/wallet.sqlite";
2233 string strTempPath = GetAppDir() + "/" +
2234 strprintf("wallet.sqlite.encrypting.%lld", GetTime());
2235 remove(strTempPath.c_str());
2236 remove((strTempPath + "-wal").c_str());
2237 remove((strTempPath + "-shm").c_str());
2238
2239 {
2240 CWalletDBSQLite dbTmp;
2241 if (!dbTmp.Open(strTempPath, strErrorRet))
2242 {
2243 remove(strTempPath.c_str());
2244 return false;
2245 }
2246 if (!dbTmp.BeginTransaction(strErrorRet))
2247 {
2248 dbTmp.Close();
2249 remove(strTempPath.c_str());
2250 return false;
2251 }
2252
2253 // The public records come from the live SQLite store -- under this
2254 // backend ScanWalletRecords() has no Berkeley DB behind it to read.
2255 CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet);
2256 bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet);
2257
2258 // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB
2259 // path (CWalletRewriteDB) writes it, so a wallet encrypted under either
2260 // backend is the same set of records. ckey uses INSERT-only to match
2261 // WriteCryptedKey; the rest overwrite.
2262 fOk = fOk &&
2263 dbTmp.WriteTypedRecord(string("walletminversion"),
2264 (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) &&
2265 dbTmp.WriteTypedRecord(make_pair(string("mkey"),
2266 (unsigned int)(nWalletMasterKeyMaxID + 1)),
2267 kMasterKey, strErrorRet, true);
2268 for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++)
2269 fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first),
2270 vCryptedKeys[i].second, strErrorRet, false);
2271 for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++)
2272 fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first),
2273 vNewPoolKeys[i].second, strErrorRet, false);
2274 if (fOk && !vchCryptedHDMasterNew.empty())
2275 fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"),
2276 vchCryptedHDMasterNew, strErrorRet, true) &&
2277 dbTmp.WriteTypedRecord(string("cryptedhdchaincode"),
2278 vchCryptedHDChainCodeNew, strErrorRet, true);
2279 if (fOk)
2280 fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true);
2281 for (size_t i = 0; fOk && i < vNewPool.size(); i++)
2282 fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first),
2283 vNewPool[i].second, strErrorRet, true);
2284
2285 if (!fOk)
2286 {
2287 string strIgnore;
2288 dbTmp.RollbackTransaction(strIgnore);
2289 dbTmp.Close();
2290 remove(strTempPath.c_str());
2291 remove((strTempPath + "-wal").c_str());
2292 remove((strTempPath + "-shm").c_str());
2293 return false;
2294 }
2295
2296 if (!dbTmp.CommitTransaction(strErrorRet))
2297 {
2298 dbTmp.Close();
2299 remove(strTempPath.c_str());
2300 remove((strTempPath + "-wal").c_str());
2301 remove((strTempPath + "-shm").c_str());
2302 return false;
2303 }
2304 string strCheckpointError;
2305 dbTmp.Checkpoint(strCheckpointError);
2306 dbTmp.Close();
2307 }
2308
2309 // The rebuilt wallet is committed and self-contained. Close the live handle
2310 // so the file can be swapped: Windows will not rename a file that still has
2311 // an open handle, and the node exits right after this.
2312 WalletSQLiteRuntimeClose();
2313
2314 string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite");
2315 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0)
2316 {
2317 strErrorRet = strprintf("could not move the original SQLite wallet to %s",
2318 strBackupPath.c_str());
2319 remove(strTempPath.c_str());
2320 return false;
2321 }
2322 // The moved-aside wallet owns any leftover write-ahead log or shared-memory
2323 // sidecar; drop stale ones so they cannot shadow the new wallet.
2324 remove((strWalletPath + "-wal").c_str());
2325 remove((strWalletPath + "-shm").c_str());
2326
2327 if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0)
2328 {
2329 rename(strBackupPath.c_str(), strWalletPath.c_str());
2330 strErrorRet = "could not install the encrypted SQLite wallet; the original was restored";
2331 remove(strTempPath.c_str());
2332 return false;
2333 }
2334 remove((strTempPath + "-wal").c_str());
2335 remove((strTempPath + "-shm").c_str());
2336
2337 strBackupRet = strBackupPath;
2338 return true;
2339}
2340
2009bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) 2341bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet)
2010{ 2342{
2011 strBackupRet.clear(); 2343 strBackupRet.clear();
@@ -2144,6 +2476,24 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
2144 vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); 2476 vNewPool.push_back(make_pair((int64)i + 1, vchPubKey));
2145 } 2477 }
2146 2478
2479 // Every ciphertext is computed; nothing has touched the store yet. Under
2480 // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic
2481 // transaction and swapping it in. The Berkeley DB path below is unchanged.
2482 if (WalletSQLiteRuntimeActive())
2483 {
2484 int nCopiedSQLite = 0;
2485 if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool,
2486 kMasterKey, vchCryptedHDMasterNew,
2487 vchCryptedHDChainCodeNew, nHDNextNew,
2488 nCopiedSQLite, strBackupRet, strErrorRet))
2489 return false;
2490 printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), "
2491 "rebuilt %d key-pool entry(s) [SQLite]\n",
2492 nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()),
2493 (int)vNewPool.size());
2494 return true;
2495 }
2496
2147 string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); 2497 string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime());
2148 string strTempPath = GetAppDir() + "/" + strTempFile; 2498 string strTempPath = GetAppDir() + "/" + strTempFile;
2149 remove(strTempPath.c_str()); 2499 remove(strTempPath.c_str());
@@ -2211,7 +2561,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
2211 DBFlush(true); 2561 DBFlush(true);
2212 2562
2213 string strWalletPath = GetAppDir() + "/wallet.dat"; 2563 string strWalletPath = GetAppDir() + "/wallet.dat";
2214 string strBackupPath = WalletEncryptBackupPath(); 2564 string strBackupPath = WalletEncryptBackupPathFor("wallet.dat");
2215 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) 2565 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0)
2216 { 2566 {
2217 strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); 2567 strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str());