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:
Diffstat (limited to 'src')
-rw-r--r--src/db.cpp91
-rw-r--r--src/db.h5
-rw-r--r--src/gui.cpp90
-rw-r--r--src/main_gui.cpp80
-rw-r--r--src/makefile.mingw2
-rw-r--r--src/selftest.cpp241
-rw-r--r--src/walletcmd.cpp119
-rw-r--r--src/walletcmd.h9
8 files changed, 590 insertions, 47 deletions
diff --git a/src/db.cpp b/src/db.cpp
index aafc0f9..949865f 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -101,10 +101,13 @@ bool WalletSQLiteRuntimeActive()
101 return pWalletSQLiteRuntime != NULL; 101 return pWalletSQLiteRuntime != NULL;
102} 102}
103 103
104bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet) 104bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist)
105{ 105{
106 strErrorRet.clear(); 106 strErrorRet.clear();
107 if (!FileExists(strPath.c_str())) 107 // Callers that load an existing wallet want the clear "does not exist"
108 // message rather than silently creating a blank one; the create path passes
109 // fMustExist=false so Open() (SQLITE_OPEN_CREATE) can make a fresh file.
110 if (fMustExist && !FileExists(strPath.c_str()))
108 { 111 {
109 strErrorRet = strprintf("SQLite wallet does not exist: %s", 112 strErrorRet = strprintf("SQLite wallet does not exist: %s",
110 strPath.c_str()); 113 strPath.c_str());
@@ -230,6 +233,47 @@ bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet)
230 return pWalletSQLiteRuntime->Checkpoint(strErrorRet); 233 return pWalletSQLiteRuntime->Checkpoint(strErrorRet);
231} 234}
232 235
236// The persistent record of which backend this datadir uses, so the choice
237// survives a restart without a command-line flag. A plain text file holding
238// "sqlite" or "bdb" -- visible and hand-editable on purpose, no hidden state.
239static string WalletBackendMarkerPath()
240{
241 return GetAppDir() + "/wallet-backend";
242}
243
244string ReadWalletBackendMarker()
245{
246 FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "rb");
247 if (!pf)
248 return "";
249 char buf[16] = {0};
250 size_t n = fread(buf, 1, sizeof(buf) - 1, pf);
251 fclose(pf);
252 string s(buf, n);
253 while (!s.empty() && isspace((unsigned char)s[s.size() - 1]))
254 s.erase(s.size() - 1);
255 while (!s.empty() && isspace((unsigned char)s[0]))
256 s.erase(0, 1);
257 if (s == "sqlite" || s == "bdb")
258 return s;
259 return "";
260}
261
262bool WriteWalletBackendMarker(const string& strBackend)
263{
264 if (strBackend != "sqlite" && strBackend != "bdb")
265 return false;
266 FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "wb");
267 if (!pf)
268 return false;
269 bool fOk = fwrite(strBackend.c_str(), 1, strBackend.size(), pf) == strBackend.size();
270 if (fputc('\n', pf) == EOF)
271 fOk = false;
272 if (fclose(pf) != 0)
273 fOk = false;
274 return fOk;
275}
276
233 277
234CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) 278CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
235{ 279{
@@ -1609,6 +1653,49 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath)
1609 return true; 1653 return true;
1610} 1654}
1611 1655
1656// Create a brand-new wallet directly in the SQLite backend. For a fresh install
1657// where SQLite is the default there is no wallet.dat to export from, so open a
1658// new wallet.sqlite (Open() creates the file and its schema) and let
1659// FinishLoadedWallet mint the first default key straight into it -- the same
1660// call the Berkeley DB create path uses, so a new wallet is identical either
1661// way. Refuses to touch an existing file, and cleans up on failure so a broken
1662// half-initialised wallet.sqlite is never left for the next start to load.
1663bool CreateNewSQLiteWallet(const string& strPath)
1664{
1665 strWalletLoadError.clear();
1666
1667 if (FileExists(strPath.c_str()))
1668 {
1669 strWalletLoadError = strprintf("SQLite wallet already exists: %s",
1670 strPath.c_str());
1671 printf("CreateNewSQLiteWallet: %s\n", strWalletLoadError.c_str());
1672 return false;
1673 }
1674
1675 string strError;
1676 if (!WalletSQLiteRuntimeOpen(strPath, strError, false))
1677 {
1678 strWalletLoadError = strError;
1679 printf("CreateNewSQLiteWallet: cannot create SQLite wallet: %s\n",
1680 strError.c_str());
1681 return false;
1682 }
1683
1684 vector<unsigned char> vchNoDefaultKey;
1685 if (!FinishLoadedWallet(vchNoDefaultKey, true))
1686 {
1687 WalletSQLiteRuntimeClose();
1688 remove(strPath.c_str());
1689 remove((strPath + "-wal").c_str());
1690 remove((strPath + "-shm").c_str());
1691 if (strWalletLoadError.empty())
1692 strWalletLoadError = "could not initialise the new SQLite wallet";
1693 return false;
1694 }
1695
1696 return true;
1697}
1698
1612 1699
1613// Write a copy of wallet.dat that can be opened anywhere. 1700// Write a copy of wallet.dat that can be opened anywhere.
1614// 1701//
diff --git a/src/db.h b/src/db.h
index 5bc94b1..c50065b 100644
--- a/src/db.h
+++ b/src/db.h
@@ -357,7 +357,7 @@ private:
357// peer discovery is entirely Nostr/.btf-based now. 357// peer discovery is entirely Nostr/.btf-based now.
358 358
359bool WalletSQLiteRuntimeActive(); 359bool WalletSQLiteRuntimeActive();
360bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet); 360bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist=true);
361void WalletSQLiteRuntimeClose(); 361void WalletSQLiteRuntimeClose();
362bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey, 362bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey,
363 vector<unsigned char>& vchValueRet); 363 vector<unsigned char>& vchValueRet);
@@ -370,6 +370,9 @@ bool WalletSQLiteRuntimeCommitTxn();
370void WalletSQLiteRuntimeRollbackTxn(); 370void WalletSQLiteRuntimeRollbackTxn();
371bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); 371bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet);
372bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); 372bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet);
373string ReadWalletBackendMarker();
374bool WriteWalletBackendMarker(const string& strBackend);
375bool CreateNewSQLiteWallet(const string& strPath);
373 376
374 377
375 378
diff --git a/src/gui.cpp b/src/gui.cpp
index 8dcb854..89a7ab6 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -1391,6 +1391,84 @@ static void DrawAboutDialog()
1391 ImGui::End(); 1391 ImGui::End();
1392} 1392}
1393 1393
1394// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the
1395// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite
1396// and records the backend marker (wallet.dat is left untouched, so a restart
1397// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB"
1398// just records that choice so the offer does not return; "Decide later" leaves
1399// no marker and asks again next start.
1400static bool g_showBackendWizard = false;
1401static std::string g_wizardStatus;
1402static bool g_wizardConverted = false;
1403
1404static void DrawBackendWizardDialog()
1405{
1406 if (!g_showBackendWizard) return;
1407 ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always);
1408 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1409 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1410 if (ImGui::Begin("Wallet Storage", &g_showBackendWizard,
1411 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1412 {
1413 if (g_wizardConverted)
1414 {
1415 ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite.");
1416 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1417 ImGui::Spacing();
1418 ImGui::TextWrapped(
1419 "Restart Bitflash to start using the SQLite wallet. Your Berkeley "
1420 "DB wallet.dat is kept untouched as a fallback.");
1421 ImGui::Spacing();
1422 if (ImGui::Button("Close", ImVec2(120.0f, 0.0f)))
1423 g_showBackendWizard = false;
1424 }
1425 else
1426 {
1427 ImGui::TextWrapped(
1428 "Bitflash can store your wallet in a new SQLite format: a single "
1429 "file that is easier to back up and does not depend on the "
1430 "database/ directory beside it.");
1431 ImGui::Spacing();
1432 ImGui::TextWrapped(
1433 "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it "
1434 "is -- nothing is deleted, and you can go back to it at any time. "
1435 "The change takes effect after you restart.");
1436 ImGui::Spacing();
1437 if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f)))
1438 {
1439 int nCopied = 0;
1440 std::string strError;
1441 if (DoConvertWalletToSQLite(nCopied, strError))
1442 {
1443 g_wizardStatus =
1444 strprintf("%d record(s) copied to wallet.sqlite.", nCopied);
1445 g_wizardConverted = true;
1446 }
1447 else
1448 g_wizardStatus = "Conversion failed: " + strError;
1449 }
1450 ImGui::SameLine();
1451 if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f)))
1452 {
1453 WriteWalletBackendMarker("bdb");
1454 g_showBackendWizard = false;
1455 }
1456 ImGui::SameLine();
1457 if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f)))
1458 g_showBackendWizard = false;
1459
1460 if (!g_wizardStatus.empty())
1461 {
1462 ImGui::Spacing();
1463 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f));
1464 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1465 ImGui::PopStyleColor();
1466 }
1467 }
1468 }
1469 ImGui::End();
1470}
1471
1394// --------------------------------------------------------------------------- 1472// ---------------------------------------------------------------------------
1395// Entry point 1473// Entry point
1396// --------------------------------------------------------------------------- 1474// ---------------------------------------------------------------------------
@@ -1457,6 +1535,17 @@ int RunGUI(int argc, char* argv[])
1457 g_mineRadio = nMineMode; 1535 g_mineRadio = nMineMode;
1458 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); 1536 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
1459 1537
1538 // First run on a Berkeley DB wallet with no recorded choice yet: offer to
1539 // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists,
1540 // or once a choice has been recorded in the backend marker.
1541 if (!WalletSQLiteRuntimeActive() &&
1542 FileExists((GetAppDir() + "/wallet.dat").c_str()) &&
1543 !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) &&
1544 ReadWalletBackendMarker().empty())
1545 {
1546 g_showBackendWizard = true;
1547 }
1548
1460 int frame = 0; 1549 int frame = 0;
1461 while (!glfwWindowShouldClose(window) && !fShutdown) 1550 while (!glfwWindowShouldClose(window) && !fShutdown)
1462 { 1551 {
@@ -1480,6 +1569,7 @@ int RunGUI(int argc, char* argv[])
1480 DrawRestorePhraseDialog(); 1569 DrawRestorePhraseDialog();
1481 DrawDiagnosticsDialog(); 1570 DrawDiagnosticsDialog();
1482 DrawAboutDialog(); 1571 DrawAboutDialog();
1572 DrawBackendWizardDialog();
1483 DrawStatusBar(); 1573 DrawStatusBar();
1484 1574
1485 ImGui::Render(); 1575 ImGui::Render();
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index af6053f..6cd8b75 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -743,14 +743,80 @@ int main(int argc, char* argv[])
743 DBFlush(true); 743 DBFlush(true);
744 return 0; 744 return 0;
745 } 745 }
746 else if (!LoadWallet()) 746 else if (strWalletBackend == "bdb")
747 { 747 {
748 // LoadWallet() explains itself into strWalletLoadError when it 748 // Explicit -walletbackend=bdb overrides the marker and the default:
749 // knows why -- an unsupported wallet format, for one, which is the 749 // force Berkeley DB (LoadWallet creates wallet.dat if none exists).
750 // case this whole path exists to make visible. 750 if (!LoadWallet())
751 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", 751 {
752 strWalletLoadError); 752 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
753 return 1; 753 strWalletLoadError);
754 return 1;
755 }
756 }
757 else
758 {
759 // No explicit -walletbackend on the command line. Resolve it from
760 // the persistent marker, then defaults: an existing wallet.sqlite
761 // stays on SQLite, an existing wallet.dat stays on Berkeley DB (the
762 // GUI offers to convert), and a fresh install starts on SQLite --
763 // the new default. wallet.dat is never touched by any of this.
764 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
765 bool fSQLiteExists = FileExists(strSQLitePath.c_str());
766 bool fWalletDatExists = FileExists((GetAppDir() + "/wallet.dat").c_str());
767 string strMarker = ReadWalletBackendMarker();
768
769 bool fUseSQLite = false;
770 bool fCreateSQLite = false;
771 if (strMarker == "sqlite")
772 {
773 if (fSQLiteExists) fUseSQLite = true;
774 else if (fWalletDatExists) fUseSQLite = false; // marker stale; real wallet is BDB
775 else { fUseSQLite = true; fCreateSQLite = true; }
776 }
777 else if (strMarker == "bdb")
778 {
779 fUseSQLite = false;
780 }
781 else // no marker recorded yet
782 {
783 if (fSQLiteExists && !fWalletDatExists) fUseSQLite = true;
784 else if (fWalletDatExists) fUseSQLite = false;
785 else { fUseSQLite = true; fCreateSQLite = true; }
786 }
787
788 if (fUseSQLite)
789 {
790 AttachTerminal();
791 bool fOk = fCreateSQLite
792 ? CreateNewSQLiteWallet(strSQLitePath)
793 : LoadWalletFromSQLiteRuntime(strSQLitePath);
794 if (!fOk)
795 {
796 FatalStartupError(fHeadlessStartup,
797 fCreateSQLite ? "Cannot create the SQLite wallet."
798 : "Cannot open the SQLite wallet.",
799 strWalletLoadError);
800 return 1;
801 }
802 // Record the choice so it is stable from here on.
803 if (ReadWalletBackendMarker() != "sqlite")
804 WriteWalletBackendMarker("sqlite");
805 fprintf(stderr, "Wallet backend: SQLite\n %s\n%s",
806 strSQLitePath.c_str(),
807 fCreateSQLite ? "A new wallet was created here.\n" : "");
808 fflush(stderr);
809 // Fall through and run the node; CWalletDB routes to this file.
810 }
811 else if (!LoadWallet())
812 {
813 // LoadWallet() explains itself into strWalletLoadError when it
814 // knows why -- an unsupported wallet format, for one, which is
815 // the case this whole path exists to make visible.
816 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
817 strWalletLoadError);
818 return 1;
819 }
754 } 820 }
755 821
756 // After the block index, so there is a chain to compare the wallet 822 // After the block index, so there is a chain to compare the wallet
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 1cb47fb..55f186b 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -113,6 +113,8 @@ tests: test_bip32.exe bitflash.exe
113 ./bitflash.exe -selftest=wallet-crypto 113 ./bitflash.exe -selftest=wallet-crypto
114 ./bitflash.exe -selftest=wallet-encrypt 114 ./bitflash.exe -selftest=wallet-encrypt
115 ./bitflash.exe -selftest=wallet-sqlite-encrypt 115 ./bitflash.exe -selftest=wallet-sqlite-encrypt
116 ./bitflash.exe -selftest=wallet-backend-default
117 ./bitflash.exe -selftest=wallet-convert
116 ./bitflash.exe -selftest=net-message 118 ./bitflash.exe -selftest=net-message
117 ./bitflash.exe -selftest=consensus-limits 119 ./bitflash.exe -selftest=consensus-limits
118 ./bitflash.exe -selftest=pool-stratum 120 ./bitflash.exe -selftest=pool-stratum
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 90a30d2..a9c73fe 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -911,6 +911,7 @@ static int RunWalletStorageSanitySelfTest()
911 vCreateArgs.push_back("-datadir=" + strScenarioDir); 911 vCreateArgs.push_back("-datadir=" + strScenarioDir);
912 vCreateArgs.push_back("-nomanagedtor"); 912 vCreateArgs.push_back("-nomanagedtor");
913 vCreateArgs.push_back("-nogui"); 913 vCreateArgs.push_back("-nogui");
914 vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat
914 vCreateArgs.push_back("-newphrase"); 915 vCreateArgs.push_back("-newphrase");
915 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); 916 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
916 nFail += Check(nCreateRet == 0 && 917 nFail += Check(nCreateRet == 0 &&
@@ -1474,6 +1475,7 @@ static int RunWalletSQLiteMigrationSelfTest()
1474 vCreateArgs.push_back("-datadir=" + strWalletDir); 1475 vCreateArgs.push_back("-datadir=" + strWalletDir);
1475 vCreateArgs.push_back("-nomanagedtor"); 1476 vCreateArgs.push_back("-nomanagedtor");
1476 vCreateArgs.push_back("-nogui"); 1477 vCreateArgs.push_back("-nogui");
1478 vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat
1477 vCreateArgs.push_back("-newphrase"); 1479 vCreateArgs.push_back("-newphrase");
1478 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); 1480 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
1479 nFail += Check(nCreateRet == 0 && 1481 nFail += Check(nCreateRet == 0 &&
@@ -2534,6 +2536,238 @@ static int RunWalletSQLiteEncryptSelfTest()
2534 return nFail == 0 ? 0 : 1; 2536 return nFail == 0 ? 0 : 1;
2535} 2537}
2536 2538
2539// Backend resolution when no -walletbackend flag is given: a fresh install
2540// starts on SQLite (the new default), an existing wallet.dat stays on Berkeley
2541// DB, the persistent marker overrides the default, and an explicit flag
2542// overrides everything. Each scenario runs a real child process.
2543static int RunWalletBackendDefaultSelfTest()
2544{
2545 fflush(stdout);
2546 printf("wallet-backend-default self-test\n");
2547
2548 std::string tmp, cwd;
2549 if (!MakeTempDir(tmp))
2550 {
2551 printf(" FAIL could not create a temporary data directory\n");
2552 return 1;
2553 }
2554 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2555 {
2556 printf(" FAIL could not move into the temporary data directory\n");
2557 RemoveTree(tmp);
2558 return 1;
2559 }
2560
2561 int nFail = 0;
2562 strSetDataDir = tmp;
2563 printf(" temp datadir: %s\n", tmp.c_str());
2564
2565#ifdef _WIN32
2566 string strExe = cwd + "\\bitflash.exe";
2567#else
2568 string strExe = cwd + "/bitflash-node";
2569#endif
2570
2571 try
2572 {
2573 // 1. Fresh datadir, no flag -> new wallet.sqlite, marker "sqlite", no wallet.dat.
2574 string d1 = tmp + "/fresh";
2575 nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1;
2576 vector<string> a1;
2577 a1.push_back("-datadir=" + d1);
2578 a1.push_back("-nomanagedtor");
2579 a1.push_back("-nogui");
2580 a1.push_back("-newaddress");
2581 int r1 = RunBitflashChild(strExe, a1);
2582 nFail += Check(r1 == 0 &&
2583 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2584 !FileExists((d1 + "/wallet.dat").c_str()) &&
2585 FileContainsText(d1 + "/wallet-backend", "sqlite"),
2586 "a fresh install starts on SQLite (new default) and records the marker") ? 0 : 1;
2587
2588 // 1b. Second run reuses it, still SQLite, still no wallet.dat.
2589 int r1b = RunBitflashChild(strExe, a1);
2590 nFail += Check(r1b == 0 &&
2591 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2592 !FileExists((d1 + "/wallet.dat").c_str()),
2593 "the recorded SQLite choice survives a restart") ? 0 : 1;
2594
2595 // 2. Existing wallet.dat, no flag -> stays on Berkeley DB.
2596 string d2 = tmp + "/existing-bdb";
2597 nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1;
2598 vector<string> a2c;
2599 a2c.push_back("-datadir=" + d2);
2600 a2c.push_back("-nomanagedtor");
2601 a2c.push_back("-nogui");
2602 a2c.push_back("-walletbackend=bdb");
2603 a2c.push_back("-newphrase");
2604 RunBitflashChild(strExe, a2c);
2605 vector<string> a2;
2606 a2.push_back("-datadir=" + d2);
2607 a2.push_back("-nomanagedtor");
2608 a2.push_back("-nogui");
2609 a2.push_back("-newaddress");
2610 int r2 = RunBitflashChild(strExe, a2);
2611 nFail += Check(r2 == 0 &&
2612 FileExists((d2 + "/wallet.dat").c_str()) &&
2613 !FileExists((d2 + "/wallet.sqlite").c_str()),
2614 "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1;
2615
2616 // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir.
2617 string d3 = tmp + "/marker-bdb";
2618 nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1;
2619 nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"),
2620 "a bdb marker can be written") ? 0 : 1;
2621 vector<string> a3;
2622 a3.push_back("-datadir=" + d3);
2623 a3.push_back("-nomanagedtor");
2624 a3.push_back("-nogui");
2625 a3.push_back("-newaddress");
2626 int r3 = RunBitflashChild(strExe, a3);
2627 nFail += Check(r3 == 0 &&
2628 FileExists((d3 + "/wallet.dat").c_str()) &&
2629 !FileExists((d3 + "/wallet.sqlite").c_str()),
2630 "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1;
2631
2632 // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat.
2633 string d4 = tmp + "/explicit-bdb";
2634 nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1;
2635 vector<string> a4;
2636 a4.push_back("-datadir=" + d4);
2637 a4.push_back("-nomanagedtor");
2638 a4.push_back("-nogui");
2639 a4.push_back("-walletbackend=bdb");
2640 a4.push_back("-newaddress");
2641 int r4 = RunBitflashChild(strExe, a4);
2642 nFail += Check(r4 == 0 &&
2643 FileExists((d4 + "/wallet.dat").c_str()) &&
2644 !FileExists((d4 + "/wallet.sqlite").c_str()),
2645 "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1;
2646 }
2647 catch (const std::exception& e)
2648 {
2649 printf(" FAIL exception: %s\n", e.what());
2650 nFail++;
2651 }
2652 catch (...)
2653 {
2654 printf(" FAIL unknown exception\n");
2655 nFail++;
2656 }
2657
2658 DBFlush(true);
2659 SetCurrentDir(cwd);
2660 RemoveTree(tmp);
2661 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2662 nFail, nFail == 1 ? "" : "s");
2663 fflush(stdout);
2664 return nFail == 0 ? 0 : 1;
2665}
2666
2667// The wizard's conversion action, headless: build a Berkeley DB wallet, run
2668// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a
2669// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then
2670// that a flagless restart opens the converted SQLite wallet.
2671static int RunWalletConvertSelfTest()
2672{
2673 fflush(stdout);
2674 printf("wallet-convert self-test\n");
2675
2676 std::string tmp, cwd;
2677 if (!MakeTempDir(tmp))
2678 {
2679 printf(" FAIL could not create a temporary data directory\n");
2680 return 1;
2681 }
2682 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2683 {
2684 printf(" FAIL could not move into the temporary data directory\n");
2685 RemoveTree(tmp);
2686 return 1;
2687 }
2688
2689 int nFail = 0;
2690 strSetDataDir = tmp;
2691 printf(" temp datadir: %s\n", tmp.c_str());
2692
2693#ifdef _WIN32
2694 string strExe = cwd + "\\bitflash.exe";
2695#else
2696 string strExe = cwd + "/bitflash-node";
2697#endif
2698
2699 try
2700 {
2701 if (!LoadWallet())
2702 throw std::runtime_error("LoadWallet failed");
2703
2704 string strError;
2705 nFail += Check(SetHDSeedFromMnemonic(
2706 "abandon abandon abandon abandon abandon abandon abandon "
2707 "abandon abandon abandon abandon about", strError),
2708 "a phrase can be installed before conversion") ? 0 : 1;
2709 TopUpKeyPool();
2710
2711 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2712 CPrivKey vchDefaultPrivKey;
2713 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
2714 "the default private key is readable before conversion") ? 0 : 1;
2715 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(),
2716 vchDefaultPrivKey.end());
2717
2718 // The conversion action the GUI button invokes.
2719 int nCopied = 0;
2720 string strConvertError;
2721 bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError);
2722 nFail += Check(fConverted && nCopied > 0,
2723 "DoConvertWalletToSQLite exports the wallet") ? 0 : 1;
2724 nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) &&
2725 FileContainsText(tmp + "/wallet-backend", "sqlite"),
2726 "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1;
2727 nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes),
2728 "wallet.dat is left untouched by the conversion") ? 0 : 1;
2729
2730 // Idempotence guard: a second conversion refuses rather than overwrite.
2731 int nCopied2 = 0;
2732 string strSecondError;
2733 nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError),
2734 "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1;
2735
2736 DBFlush(true);
2737
2738 // A flagless restart now opens the converted SQLite wallet (the marker
2739 // routes it) and finds the same recovery phrase.
2740 string strAudit = tmp + "/converted-recovery-audit.txt";
2741 vector<string> vAuditArgs;
2742 vAuditArgs.push_back("-datadir=" + tmp);
2743 vAuditArgs.push_back("-nomanagedtor");
2744 vAuditArgs.push_back("-nogui");
2745 vAuditArgs.push_back("-recoveryaudit");
2746 int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit);
2747 nFail += Check(nAuditRet == 0 &&
2748 FileContainsText(strAudit, "recovery phrase: present"),
2749 "a flagless restart opens the converted SQLite wallet") ? 0 : 1;
2750 }
2751 catch (const std::exception& e)
2752 {
2753 printf(" FAIL exception: %s\n", e.what());
2754 nFail++;
2755 }
2756 catch (...)
2757 {
2758 printf(" FAIL unknown exception\n");
2759 nFail++;
2760 }
2761
2762 DBFlush(true);
2763 SetCurrentDir(cwd);
2764 RemoveTree(tmp);
2765 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2766 nFail, nFail == 1 ? "" : "s");
2767 fflush(stdout);
2768 return nFail == 0 ? 0 : 1;
2769}
2770
2537static int RunWalletPortabilitySelfTest() 2771static int RunWalletPortabilitySelfTest()
2538{ 2772{
2539 fflush(stdout); 2773 fflush(stdout);
@@ -2576,6 +2810,7 @@ static int RunWalletPortabilitySelfTest()
2576 vCreateArgs.push_back("-datadir=" + strOriginal); 2810 vCreateArgs.push_back("-datadir=" + strOriginal);
2577 vCreateArgs.push_back("-nomanagedtor"); 2811 vCreateArgs.push_back("-nomanagedtor");
2578 vCreateArgs.push_back("-nogui"); 2812 vCreateArgs.push_back("-nogui");
2813 vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability
2579 vCreateArgs.push_back("-newphrase"); 2814 vCreateArgs.push_back("-newphrase");
2580 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); 2815 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
2581 nFail += Check(nCreateRet == 0 && 2816 nFail += Check(nCreateRet == 0 &&
@@ -3265,6 +3500,10 @@ int RunSelfTest(const std::string& name)
3265 return RunWalletEncryptSelfTest(); 3500 return RunWalletEncryptSelfTest();
3266 if (name == "wallet-sqlite-encrypt") 3501 if (name == "wallet-sqlite-encrypt")
3267 return RunWalletSQLiteEncryptSelfTest(); 3502 return RunWalletSQLiteEncryptSelfTest();
3503 if (name == "wallet-backend-default")
3504 return RunWalletBackendDefaultSelfTest();
3505 if (name == "wallet-convert")
3506 return RunWalletConvertSelfTest();
3268 if (name == "wallet-portability") 3507 if (name == "wallet-portability")
3269 return RunWalletPortabilitySelfTest(); 3508 return RunWalletPortabilitySelfTest();
3270 if (name == "net-message") 3509 if (name == "net-message")
@@ -3281,6 +3520,6 @@ int RunSelfTest(const std::string& name)
3281 return RunManagedTorSelfTest(); 3520 return RunManagedTorSelfTest();
3282 3521
3283 printf("Unknown self-test '%s'\n", name.c_str()); 3522 printf("Unknown self-test '%s'\n", name.c_str());
3284 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); 3523 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n");
3285 return 1; 3524 return 1;
3286} 3525}
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index 374b2cc..1f1fd27 100644
--- a/src/walletcmd.cpp
+++ b/src/walletcmd.cpp
@@ -1070,79 +1070,126 @@ public:
1070 1070
1071} // namespace 1071} // namespace
1072 1072
1073int CmdWalletSQLiteExport(const std::string& strDest) 1073// Export the currently-loaded wallet to a SQLite file. Shared by the
1074// -walletsqliteexport command and the GUI conversion wizard: opens the
1075// destination, copies every record inside one transaction, checkpoints, and
1076// verifies the stored count matches. Returns false with strErrorRet set on any
1077// failure and leaves no partial file behind; the source wallet is only read.
1078bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet,
1079 std::string& strErrorRet)
1074{ 1080{
1075 AttachTerminal(); 1081 nCopiedRet = 0;
1076
1077 if (strDest.empty()) 1082 if (strDest.empty())
1078 { 1083 {
1079 fprintf(stderr, "Missing SQLite wallet export path.\n"); 1084 strErrorRet = "no SQLite export path was given";
1080 return 1; 1085 return false;
1081 } 1086 }
1082 if (FileExists(strDest.c_str())) 1087 if (FileExists(strDest.c_str()))
1083 { 1088 {
1084 fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", 1089 strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str());
1085 strDest.c_str()); 1090 return false;
1086 return 1;
1087 } 1091 }
1088 1092
1089 string strError;
1090 CWalletDBSQLite db; 1093 CWalletDBSQLite db;
1091 if (!db.Open(strDest, strError)) 1094 if (!db.Open(strDest, strErrorRet))
1092 { 1095 {
1093 RemoveSQLiteExportFiles(strDest); 1096 RemoveSQLiteExportFiles(strDest);
1094 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); 1097 return false;
1095 return 1;
1096 } 1098 }
1097 if (!db.BeginTransaction(strError)) 1099 if (!db.BeginTransaction(strErrorRet))
1098 { 1100 {
1099 db.Close(); 1101 db.Close();
1100 RemoveSQLiteExportFiles(strDest); 1102 RemoveSQLiteExportFiles(strDest);
1101 fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); 1103 return false;
1102 return 1;
1103 } 1104 }
1104 1105
1105 int nCopied = 0; 1106 CWalletSQLiteExportVisitor visitor(db, nCopiedRet);
1106 CWalletSQLiteExportVisitor visitor(db, nCopied); 1107 if (!ScanWalletRecords(visitor, strErrorRet))
1107 if (!ScanWalletRecords(visitor, strError))
1108 { 1108 {
1109 string strRollbackError; 1109 string strRollbackError;
1110 db.RollbackTransaction(strRollbackError); 1110 db.RollbackTransaction(strRollbackError);
1111 db.Close(); 1111 db.Close();
1112 RemoveSQLiteExportFiles(strDest); 1112 RemoveSQLiteExportFiles(strDest);
1113 fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); 1113 return false;
1114 return 1;
1115 } 1114 }
1116 if (!db.CommitTransaction(strError)) 1115 if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet))
1117 { 1116 {
1118 db.Close(); 1117 db.Close();
1119 RemoveSQLiteExportFiles(strDest); 1118 RemoveSQLiteExportFiles(strDest);
1120 fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); 1119 return false;
1121 return 1;
1122 } 1120 }
1123 if (!db.Checkpoint(strError)) 1121
1122 int nCount = 0;
1123 if (!db.CountRecords(nCount, strErrorRet))
1124 { 1124 {
1125 db.Close(); 1125 db.Close();
1126 RemoveSQLiteExportFiles(strDest); 1126 RemoveSQLiteExportFiles(strDest);
1127 fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); 1127 return false;
1128 return 1;
1129 } 1128 }
1130 1129 if (nCount != nCopiedRet)
1131 int nCount = 0;
1132 if (!db.CountRecords(nCount, strError))
1133 { 1130 {
1131 strErrorRet = strprintf("export count mismatch: copied %d, stored %d",
1132 nCopiedRet, nCount);
1134 db.Close(); 1133 db.Close();
1135 RemoveSQLiteExportFiles(strDest); 1134 RemoveSQLiteExportFiles(strDest);
1136 fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", 1135 return false;
1137 strError.c_str()); 1136 }
1137 db.Close();
1138 return true;
1139}
1140
1141// Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export
1142// wallet.dat to <datadir>/wallet.sqlite, then record the backend marker so the
1143// next start opens SQLite. wallet.dat is never modified -- it stays as the
1144// fallback. Refuses if already on SQLite or if a wallet.sqlite already exists.
1145bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet)
1146{
1147 nCopiedRet = 0;
1148 if (WalletSQLiteRuntimeActive())
1149 {
1150 strErrorRet = "this wallet is already on the SQLite backend";
1151 return false;
1152 }
1153 string strDest = GetAppDir() + "/wallet.sqlite";
1154 if (FileExists(strDest.c_str()))
1155 {
1156 strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str());
1157 return false;
1158 }
1159 if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet))
1160 return false;
1161 if (!WriteWalletBackendMarker("sqlite"))
1162 {
1163 strErrorRet = "the wallet was exported but the backend choice could not be saved";
1164 return false;
1165 }
1166 return true;
1167}
1168
1169int CmdWalletSQLiteExport(const std::string& strDest)
1170{
1171 AttachTerminal();
1172
1173 // Keep the command's own specific messages for the two conditions the
1174 // shared helper folds into a generic error, so scripts and the tests that
1175 // read this output see the same wording as before.
1176 if (strDest.empty())
1177 {
1178 fprintf(stderr, "Missing SQLite wallet export path.\n");
1138 return 1; 1179 return 1;
1139 } 1180 }
1140 if (nCount != nCopied) 1181 if (FileExists(strDest.c_str()))
1141 { 1182 {
1142 db.Close(); 1183 fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n",
1143 RemoveSQLiteExportFiles(strDest); 1184 strDest.c_str());
1144 fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", 1185 return 1;
1145 nCopied, nCount); 1186 }
1187
1188 int nCopied = 0;
1189 string strError;
1190 if (!ExportActiveWalletToSQLite(strDest, nCopied, strError))
1191 {
1192 fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str());
1146 return 1; 1193 return 1;
1147 } 1194 }
1148 1195
diff --git a/src/walletcmd.h b/src/walletcmd.h
index d993856..38cbfdb 100644
--- a/src/walletcmd.h
+++ b/src/walletcmd.h
@@ -75,6 +75,15 @@ int CmdWalletStorageCheck();
75// not change which backend the runtime wallet opens. 75// not change which backend the runtime wallet opens.
76int CmdWalletSQLiteExport(const std::string& strDest); 76int CmdWalletSQLiteExport(const std::string& strDest);
77 77
78// Export the currently-loaded wallet to a SQLite file (shared by the command
79// above and the GUI wizard). Returns false with an error on any failure.
80bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet,
81 std::string& strErrorRet);
82
83// Convert the running Berkeley DB wallet to SQLite and record the backend
84// marker so the next start opens it. wallet.dat is left untouched as fallback.
85bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet);
86
78// Diagnostic/migration staging: compare a SQLite wallet export with the current 87// Diagnostic/migration staging: compare a SQLite wallet export with the current
79// wallet.dat raw records. Prints counts only, never record keys or values. 88// wallet.dat raw records. Prints counts only, never record keys or values.
80int CmdWalletSQLiteVerify(const std::string& strPath); 89int CmdWalletSQLiteVerify(const std::string& strPath);