diff options
| author | bitflash <dev@bitflash.network> | 2026-08-18 14:44:11 -0300 |
|---|---|---|
| committer | bitflash <dev@bitflash.network> | 2026-08-18 14:44:11 -0300 |
| commit | 84a60275569dca5d1fb12db38ce184caf7487ed6 (patch) | |
| tree | 23e39a26b340324a7a33a115aded3b70b5332816 | |
| parent | 440d7f393d74f309d29e1d10ba1f11c41c869226 (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.
| -rw-r--r-- | src/db.cpp | 112 | ||||
| -rw-r--r-- | src/db.h | 2 | ||||
| -rw-r--r-- | src/main_gui.cpp | 50 | ||||
| -rw-r--r-- | src/selftest.cpp | 60 |
4 files changed, 218 insertions, 6 deletions
| @@ -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. | ||
| 205 | bool 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 | |||
| 223 | bool 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 | ||
| 202 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | 234 | CDB::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 | ||
| 1355 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) | 1387 | bool 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. | ||
| 1634 | static 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 | |||
| 1587 | bool BackupWallet(const string& strDest) | 1695 | bool 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()); |
| @@ -368,6 +368,8 @@ bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey); | |||
| 368 | bool WalletSQLiteRuntimeBeginTxn(); | 368 | bool WalletSQLiteRuntimeBeginTxn(); |
| 369 | bool WalletSQLiteRuntimeCommitTxn(); | 369 | bool WalletSQLiteRuntimeCommitTxn(); |
| 370 | void WalletSQLiteRuntimeRollbackTxn(); | 370 | void WalletSQLiteRuntimeRollbackTxn(); |
| 371 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); | ||
| 372 | bool 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 | { |