upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbitflash <dev@bitflash.network>2026-08-18 14:44:11 -0300
committerbitflash <dev@bitflash.network>2026-08-18 14:44:11 -0300
commit84a60275569dca5d1fb12db38ce184caf7487ed6 (patch)
tree23e39a26b340324a7a33a115aded3b70b5332816 /src
parent440d7f393d74f309d29e1d10ba1f11c41c869226 (diff)
wallet: one-shot storage commands honour -walletbackend=sqlite
-walletstorageaudit, -walletstoragecheck and -backupwallet all read wallet.dat regardless of the selected backend. Under -walletbackend=sqlite that is wrong, and for backup it is dangerous: it silently backs up the stale plaintext wallet.dat instead of the live wallet.sqlite. - ScanWalletRecords() routes to the active SQLite store instead of a Berkeley DB cursor (it returned an error under the runtime before). This is the single choke point audit and check scan through. - WalletSQLiteRuntimeOpenReadOnly() opens wallet.sqlite for scanning without loading or validating records, so the pre-load diagnostics can inspect a wallet the loader would refuse. main_gui opens it for audit/check when the SQLite backend is selected (and rejects an unknown backend value there too). - BackupWallet() copies wallet.sqlite (checkpoint, then the temp/old/rename dance; a checkpointed SQLite file is self-contained, no lsn_reset). wallet-sqlite-encrypt self-test proves routing on a datadir where the SQLite wallet is encrypted and wallet.dat beside it is still plaintext: audit with the flag reports the encrypted seed, without it reports the plaintext one; the SQLite backup carries no plaintext key and loadchecks as a standalone wallet.
Diffstat (limited to 'src')
-rw-r--r--src/db.cpp112
-rw-r--r--src/db.h2
-rw-r--r--src/main_gui.cpp50
-rw-r--r--src/selftest.cpp60
4 files changed, 218 insertions, 6 deletions
diff --git a/src/db.cpp b/src/db.cpp
index c16edb4..aafc0f9 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -198,6 +198,38 @@ void WalletSQLiteRuntimeRollbackTxn()
198 pWalletSQLiteRuntime->RollbackTransaction(strError); 198 pWalletSQLiteRuntime->RollbackTransaction(strError);
199} 199}
200 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
201 233
202CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) 234CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
203{ 235{
@@ -1354,6 +1386,16 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
1354 1386
1355bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) 1387bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet)
1356{ 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
1357 class CWalletScanDB : public CWalletDB 1399 class CWalletScanDB : public CWalletDB
1358 { 1400 {
1359 public: 1401 public:
@@ -1584,11 +1626,81 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath)
1584// 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
1585// 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
1586// 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
1587bool BackupWallet(const string& strDest) 1695bool BackupWallet(const string& strDest)
1588{ 1696{
1589 if (strDest.empty()) 1697 if (strDest.empty())
1590 return error("BackupWallet() : no destination given\n"); 1698 return error("BackupWallet() : no destination given\n");
1591 1699
1700 // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat.
1701 if (WalletSQLiteRuntimeActive())
1702 return BackupWalletSQLite(strDest);
1703
1592 string strSrc = GetAppDir() + "/wallet.dat"; 1704 string strSrc = GetAppDir() + "/wallet.dat";
1593 if (!FileExists(strSrc.c_str())) 1705 if (!FileExists(strSrc.c_str()))
1594 return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); 1706 return error("BackupWallet() : %s does not exist\n", strSrc.c_str());
diff --git a/src/db.h b/src/db.h
index 1a272b9..5bc94b1 100644
--- a/src/db.h
+++ b/src/db.h
@@ -368,6 +368,8 @@ bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey);
368bool WalletSQLiteRuntimeBeginTxn(); 368bool WalletSQLiteRuntimeBeginTxn();
369bool WalletSQLiteRuntimeCommitTxn(); 369bool WalletSQLiteRuntimeCommitTxn();
370void WalletSQLiteRuntimeRollbackTxn(); 370void WalletSQLiteRuntimeRollbackTxn();
371bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet);
372bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet);
371 373
372 374
373 375
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 4be4995..af6053f 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -524,19 +524,57 @@ int main(int argc, char* argv[])
524 // explain wallet.dat states that the normal wallet loader may refuse. 524 // explain wallet.dat states that the normal wallet loader may refuse.
525 string strWalletStorageAuditJson = 525 string strWalletStorageAuditJson =
526 argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); 526 argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson");
527 if (arg(argc,argv,"/walletstorageaudit") || 527 bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") ||
528 arg(argc,argv,"-walletstorageaudit") || 528 arg(argc,argv,"-walletstorageaudit") ||
529 !strWalletStorageAuditJson.empty()) 529 !strWalletStorageAuditJson.empty();
530 bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") ||
531 arg(argc,argv,"-walletstoragecheck");
532
533 // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite
534 // the live wallet is wallet.sqlite, so open it read-only as the active
535 // store and let the same scan walk the right file. This runs before the
536 // normal backend resolution further down, so validate the value here too.
537 string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend");
538 if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() &&
539 strDiagBackend != "sqlite" && strDiagBackend != "bdb")
540 {
541 AttachTerminal();
542 fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite "
543 "or -walletbackend=bdb.\n", strDiagBackend.c_str());
544 return 1;
545 }
546 bool fDiagSQLite = (strDiagBackend == "sqlite") &&
547 (fDiagWantsAudit || fDiagWantsCheck);
548 if (fDiagSQLite)
549 {
550 AttachTerminal();
551 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
552 string strOpenErr;
553 if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr))
554 {
555 fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n",
556 strOpenErr.c_str());
557 return 1;
558 }
559 }
560
561 if (fDiagWantsAudit)
530 { 562 {
531 int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); 563 int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson);
532 DBFlush(true); 564 if (fDiagSQLite)
565 WalletSQLiteRuntimeClose();
566 else
567 DBFlush(true);
533 return nRet; 568 return nRet;
534 } 569 }
535 570
536 if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) 571 if (fDiagWantsCheck)
537 { 572 {
538 int nRet = CmdWalletStorageCheck(); 573 int nRet = CmdWalletStorageCheck();
539 DBFlush(true); 574 if (fDiagSQLite)
575 WalletSQLiteRuntimeClose();
576 else
577 DBFlush(true);
540 return nRet; 578 return nRet;
541 } 579 }
542 580
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 7ad5a53..90a30d2 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -2453,6 +2453,66 @@ static int RunWalletSQLiteEncryptSelfTest()
2453 nFail += Check(nAuditRet == 0 && 2453 nFail += Check(nAuditRet == 0 &&
2454 FileContainsText(strAudit, "recovery phrase: present"), 2454 FileContainsText(strAudit, "recovery phrase: present"),
2455 "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; 2455 "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1;
2456
2457 // One-shot storage commands honour -walletbackend. The SQLite wallet is
2458 // encrypted; the wallet.dat beside it is still plaintext. Same datadir,
2459 // opposite answers -> the flag routes the diagnostic to the right file.
2460 string strSqliteAuditJson = tmp + "/sqlite-audit.json";
2461 vector<string> vSqliteAuditArgs;
2462 vSqliteAuditArgs.push_back("-datadir=" + tmp);
2463 vSqliteAuditArgs.push_back("-walletbackend=sqlite");
2464 vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson);
2465 vSqliteAuditArgs.push_back("-nogui");
2466 int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs);
2467 nFail += Check(nSqliteAuditRet == 0 &&
2468 FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") &&
2469 FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""),
2470 "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1;
2471
2472 string strBdbAuditJson = tmp + "/bdb-audit.json";
2473 vector<string> vBdbAuditArgs;
2474 vBdbAuditArgs.push_back("-datadir=" + tmp);
2475 vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson);
2476 vBdbAuditArgs.push_back("-nogui");
2477 int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs);
2478 nFail += Check(nBdbAuditRet == 0 &&
2479 FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") &&
2480 FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""),
2481 "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1;
2482
2483 string strSqliteCheckOut = tmp + "/sqlite-check.txt";
2484 vector<string> vSqliteCheckArgs;
2485 vSqliteCheckArgs.push_back("-datadir=" + tmp);
2486 vSqliteCheckArgs.push_back("-walletbackend=sqlite");
2487 vSqliteCheckArgs.push_back("-walletstoragecheck");
2488 vSqliteCheckArgs.push_back("-nogui");
2489 int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut);
2490 nFail += Check(nSqliteCheckRet == 0,
2491 "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1;
2492
2493 // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat.
2494 // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds
2495 // none of the plaintext key that wallet.dat still contains.
2496 string strBackupDest = tmp + "/backup.sqlite";
2497 vector<string> vBackupArgs;
2498 vBackupArgs.push_back("-datadir=" + tmp);
2499 vBackupArgs.push_back("-walletbackend=sqlite");
2500 vBackupArgs.push_back("-backupwallet=" + strBackupDest);
2501 vBackupArgs.push_back("-nogui");
2502 int nBackupRet = RunBitflashChild(strExe, vBackupArgs);
2503 nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()),
2504 "-backupwallet under SQLite writes a backup file") ? 0 : 1;
2505 nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes),
2506 "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1;
2507
2508 string strBackupLoadCheck = tmp + "/backup-loadcheck.txt";
2509 vector<string> vLoadCheckArgs;
2510 vLoadCheckArgs.push_back("-datadir=" + tmp);
2511 vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest);
2512 vLoadCheckArgs.push_back("-nogui");
2513 int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck);
2514 nFail += Check(nBackupLoadRet == 0,
2515 "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1;
2456 } 2516 }
2457 catch (const std::exception& e) 2517 catch (const std::exception& e)
2458 { 2518 {