diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-01 14:45:26 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 14:45:26 -0300 |
| commit | 9da2183bb16e4c32cafbb71eeadc7f112b48ab05 (patch) | |
| tree | 090e5b471b13590c213d64259db21d6ed3a893c9 | |
| parent | 07ba3481e2624d85eaf5ff5808bd2d0280bcbb22 (diff) | |
Add wallet safety backup panel (#89)
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
| -rw-r--r-- | README.md | 23 | ||||
| -rw-r--r-- | src/gui.cpp | 145 |
2 files changed, 158 insertions, 10 deletions
| @@ -88,15 +88,20 @@ It loads the wallet, writes the copy, and exits without starting the node. If | |||
| 88 | you would rather copy by hand, shut the node down first and take the **whole** | 88 | you would rather copy by hand, shut the node down first and take the **whole** |
| 89 | data directory, not just `wallet.dat`. | 89 | data directory, not just `wallet.dat`. |
| 90 | 90 | ||
| 91 | **Every backup is a snapshot, so repeat it.** Keys are generated when they are | 91 | **Every backup is still a snapshot, but it now has a safety margin.** The wallet |
| 92 | needed rather than derived from a seed, so a backup contains only the keys that | 92 | keeps a pool of 100 pre-generated keys. A backup contains those keys, so it |
| 93 | existed the moment it was taken. Mine a block or receive to a new address | 93 | covers the next 100 mining rewards or receive addresses the node hands out after |
| 94 | afterwards, and those coins sit on-chain at an address whose private key is in | 94 | the backup. That makes a normal backup much safer than the original 0.1.0 |
| 95 | no backup you hold. Back up again whenever you use a new address. | 95 | wallet, where the very next block could land on a key the backup did not have. |
| 96 | 96 | ||
| 97 | The second point is the pre-BIP32 problem that deterministic wallets were | 97 | It is not a seed phrase. Heavy use can drain the key pool, and deterministic |
| 98 | invented to solve; see [#47](https://github.com/Bitflash-sh/bitflash/issues/47). | 98 | wallet restore is still tracked in [#47](https://github.com/Bitflash-sh/bitflash/issues/47). |
| 99 | Until that lands, repetition is the only defence. | 99 | Back up again after mining for a while, after creating many receive addresses, |
| 100 | and before moving the wallet to another machine. | ||
| 101 | |||
| 102 | The GUI has a **Backup Wallet** button and a **Wallet Safety** view. Use that if | ||
| 103 | you do not want to run the command by hand; it fills the key pool before writing | ||
| 104 | the backup. | ||
| 100 | 105 | ||
| 101 | ### When wallet.dat itself will not open | 106 | ### When wallet.dat itself will not open |
| 102 | 107 | ||
diff --git a/src/gui.cpp b/src/gui.cpp index e2bb5cf..df61bb1 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -51,11 +51,16 @@ static bool g_showSend = false; | |||
| 51 | static bool g_showOptions = false; | 51 | static bool g_showOptions = false; |
| 52 | static bool g_showAbout = false; | 52 | static bool g_showAbout = false; |
| 53 | static bool g_showDiagnostics = false; | 53 | static bool g_showDiagnostics = false; |
| 54 | static bool g_showWalletSafety = false; | ||
| 54 | static bool g_needRefresh = true; | 55 | static bool g_needRefresh = true; |
| 55 | 56 | ||
| 56 | static char g_sendAddr[128] = {}; | 57 | static char g_sendAddr[128] = {}; |
| 57 | static char g_sendAmount[32] = {}; | 58 | static char g_sendAmount[32] = {}; |
| 58 | static std::string g_sendStatus; | 59 | static std::string g_sendStatus; |
| 60 | static char g_backupPath[512] = {}; | ||
| 61 | static std::string g_backupStatus; | ||
| 62 | static int64 g_lastWalletBackup = 0; | ||
| 63 | static bool g_walletSafetyLoaded = false; | ||
| 59 | static char g_participantPool[256] = {}; | 64 | static char g_participantPool[256] = {}; |
| 60 | static char g_poolName[128] = {}; | 65 | static char g_poolName[128] = {}; |
| 61 | static char g_poolFee[32] = {}; | 66 | static char g_poolFee[32] = {}; |
| @@ -115,8 +120,38 @@ static std::string PoolLabel(const BtfPoolAnnouncement& ann) | |||
| 115 | return o.str(); | 120 | return o.str(); |
| 116 | } | 121 | } |
| 117 | 122 | ||
| 123 | static std::string BackupTimeSuffix() | ||
| 124 | { | ||
| 125 | return strprintf("%lld", (long long)GetTime()); | ||
| 126 | } | ||
| 127 | |||
| 128 | static void SetDefaultBackupPath() | ||
| 129 | { | ||
| 130 | std::string path = GetAppDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; | ||
| 131 | strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); | ||
| 132 | g_backupPath[sizeof(g_backupPath)-1] = '\0'; | ||
| 133 | } | ||
| 134 | |||
| 135 | static void LoadWalletSafetyState() | ||
| 136 | { | ||
| 137 | if (g_walletSafetyLoaded) | ||
| 138 | return; | ||
| 139 | CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); | ||
| 140 | SetDefaultBackupPath(); | ||
| 141 | g_walletSafetyLoaded = true; | ||
| 142 | } | ||
| 143 | |||
| 144 | static int KeyPoolCount() | ||
| 145 | { | ||
| 146 | int n = 0; | ||
| 147 | CRITICAL_BLOCK(cs_keyPool) | ||
| 148 | n = (int)mapKeyPool.size(); | ||
| 149 | return n; | ||
| 150 | } | ||
| 151 | |||
| 118 | static void RefreshWallet() | 152 | static void RefreshWallet() |
| 119 | { | 153 | { |
| 154 | LoadWalletSafetyState(); | ||
| 120 | std::vector<unsigned char> vchPubKey; | 155 | std::vector<unsigned char> vchPubKey; |
| 121 | if (CWalletDB("r").ReadDefaultKey(vchPubKey)) | 156 | if (CWalletDB("r").ReadDefaultKey(vchPubKey)) |
| 122 | g_myAddress = PubKeyToAddress(vchPubKey); | 157 | g_myAddress = PubKeyToAddress(vchPubKey); |
| @@ -307,10 +342,12 @@ static void DrawMainWindow() | |||
| 307 | // ---- Menu bar ---- | 342 | // ---- Menu bar ---- |
| 308 | if (ImGui::BeginMenuBar()) { | 343 | if (ImGui::BeginMenuBar()) { |
| 309 | if (ImGui::BeginMenu("File")) { | 344 | if (ImGui::BeginMenu("File")) { |
| 345 | if (ImGui::MenuItem("Backup Wallet")) g_showWalletSafety = true; | ||
| 310 | if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); | 346 | if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); |
| 311 | ImGui::EndMenu(); | 347 | ImGui::EndMenu(); |
| 312 | } | 348 | } |
| 313 | if (ImGui::MenuItem("Options")) g_showOptions = true; | 349 | if (ImGui::MenuItem("Options")) g_showOptions = true; |
| 350 | if (ImGui::MenuItem("Wallet Safety")) g_showWalletSafety = true; | ||
| 314 | if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; | 351 | if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; |
| 315 | if (ImGui::MenuItem("About")) g_showAbout = true; | 352 | if (ImGui::MenuItem("About")) g_showAbout = true; |
| 316 | ImGui::EndMenuBar(); | 353 | ImGui::EndMenuBar(); |
| @@ -340,6 +377,9 @@ static void DrawMainWindow() | |||
| 340 | if (ImGui::Button(" Send Coins ")) g_showSend = true; | 377 | if (ImGui::Button(" Send Coins ")) g_showSend = true; |
| 341 | ImGui::PopStyleColor(2); | 378 | ImGui::PopStyleColor(2); |
| 342 | 379 | ||
| 380 | ImGui::SameLine(0.0f, 8.0f); | ||
| 381 | if (ImGui::Button(" Backup Wallet ")) g_showWalletSafety = true; | ||
| 382 | |||
| 343 | ImGui::SameLine(0.0f, 20.0f); | 383 | ImGui::SameLine(0.0f, 20.0f); |
| 344 | if (fGenerateBitcoins) { | 384 | if (fGenerateBitcoins) { |
| 345 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); | 385 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); |
| @@ -701,7 +741,7 @@ static void DrawOptionsDialog() | |||
| 701 | } | 741 | } |
| 702 | wasOpen = true; | 742 | wasOpen = true; |
| 703 | 743 | ||
| 704 | ImGui::SetNextWindowSize(ImVec2(500.0f, 335.0f), ImGuiCond_Always); | 744 | ImGui::SetNextWindowSize(ImVec2(540.0f, 385.0f), ImGuiCond_Always); |
| 705 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | 745 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), |
| 706 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | 746 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); |
| 707 | if (ImGui::Begin("Options", &g_showOptions, | 747 | if (ImGui::Begin("Options", &g_showOptions, |
| @@ -722,6 +762,14 @@ static void DrawOptionsDialog() | |||
| 722 | ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); | 762 | ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); |
| 723 | ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); | 763 | ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); |
| 724 | 764 | ||
| 765 | if (g_mineRadio != MINE_RELAY && g_lastWalletBackup == 0) { | ||
| 766 | ImGui::Spacing(); | ||
| 767 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 768 | ImGui::TextWrapped("Back up this wallet before mining. The key pool covers the next %d generated addresses, but only after you save a backup that contains those keys.", KEYPOOL_SIZE); | ||
| 769 | ImGui::PopStyleColor(); | ||
| 770 | if (ImGui::SmallButton("Backup now")) g_showWalletSafety = true; | ||
| 771 | } | ||
| 772 | |||
| 725 | if (g_mineRadio == MINE_OPERATOR) { | 773 | if (g_mineRadio == MINE_OPERATOR) { |
| 726 | ImGui::Spacing(); | 774 | ImGui::Spacing(); |
| 727 | ImGui::SeparatorText("Pool Announcement"); | 775 | ImGui::SeparatorText("Pool Announcement"); |
| @@ -822,6 +870,100 @@ static void DrawOptionsDialog() | |||
| 822 | } | 870 | } |
| 823 | 871 | ||
| 824 | // --------------------------------------------------------------------------- | 872 | // --------------------------------------------------------------------------- |
| 873 | // Wallet safety dialog | ||
| 874 | // --------------------------------------------------------------------------- | ||
| 875 | static void DrawWalletSafetyDialog() | ||
| 876 | { | ||
| 877 | if (!g_showWalletSafety) return; | ||
| 878 | LoadWalletSafetyState(); | ||
| 879 | |||
| 880 | ImGui::SetNextWindowSize(ImVec2(650.0f, 390.0f), ImGuiCond_Always); | ||
| 881 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 882 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 883 | if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, | ||
| 884 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 885 | { | ||
| 886 | ImGui::SeparatorText("Backup"); | ||
| 887 | if (g_lastWalletBackup > 0) | ||
| 888 | ImGui::Text("Last GUI backup: %s", DateTimeStr(g_lastWalletBackup).c_str()); | ||
| 889 | else | ||
| 890 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | ||
| 891 | "Last GUI backup: never"); | ||
| 892 | |||
| 893 | ImGui::TextDisabled("Data directory:"); | ||
| 894 | ImGui::SameLine(); | ||
| 895 | ImGui::TextWrapped("%s", GetAppDir().c_str()); | ||
| 896 | |||
| 897 | ImGui::Spacing(); | ||
| 898 | ImGui::Text("Backup file:"); | ||
| 899 | ImGui::SetNextItemWidth(-1.0f); | ||
| 900 | ImGui::InputText("##backup-path", g_backupPath, sizeof(g_backupPath)); | ||
| 901 | |||
| 902 | if (ImGui::Button("Use new default name", ImVec2(170.0f, 0.0f))) | ||
| 903 | SetDefaultBackupPath(); | ||
| 904 | ImGui::SameLine(); | ||
| 905 | if (ImGui::Button("Copy data dir", ImVec2(120.0f, 0.0f))) | ||
| 906 | ImGui::SetClipboardText(GetAppDir().c_str()); | ||
| 907 | |||
| 908 | ImGui::Spacing(); | ||
| 909 | if (ImGui::Button("Write Backup", ImVec2(150.0f, 0.0f))) | ||
| 910 | { | ||
| 911 | std::string path = g_backupPath; | ||
| 912 | if (path.empty()) | ||
| 913 | { | ||
| 914 | g_backupStatus = "Choose a backup file first."; | ||
| 915 | } | ||
| 916 | else if (FileExists(path.c_str())) | ||
| 917 | { | ||
| 918 | g_backupStatus = "Refusing to overwrite an existing file. Use a new name."; | ||
| 919 | } | ||
| 920 | else | ||
| 921 | { | ||
| 922 | TopUpKeyPool(); | ||
| 923 | if (BackupWallet(path)) | ||
| 924 | { | ||
| 925 | g_lastWalletBackup = GetTime(); | ||
| 926 | CWalletDB().WriteSetting("nLastWalletBackup", g_lastWalletBackup); | ||
| 927 | g_backupStatus = "Backup written: " + path; | ||
| 928 | SetDefaultBackupPath(); | ||
| 929 | } | ||
| 930 | else | ||
| 931 | { | ||
| 932 | g_backupStatus = "Backup failed. See debug.log for the detailed error."; | ||
| 933 | } | ||
| 934 | } | ||
| 935 | } | ||
| 936 | |||
| 937 | if (!g_backupStatus.empty()) | ||
| 938 | { | ||
| 939 | ImVec4 col = g_backupStatus.find("Backup written") == 0 | ||
| 940 | ? ImVec4(0.35f, 1.0f, 0.45f, 1.0f) | ||
| 941 | : ImVec4(1.0f, 0.45f, 0.35f, 1.0f); | ||
| 942 | ImGui::PushStyleColor(ImGuiCol_Text, col); | ||
| 943 | ImGui::TextWrapped("%s", g_backupStatus.c_str()); | ||
| 944 | ImGui::PopStyleColor(); | ||
| 945 | } | ||
| 946 | |||
| 947 | ImGui::Spacing(); | ||
| 948 | ImGui::SeparatorText("Key Pool"); | ||
| 949 | int nPool = KeyPoolCount(); | ||
| 950 | ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE); | ||
| 951 | ImGui::ProgressBar(KEYPOOL_SIZE > 0 ? (float)nPool / (float)KEYPOOL_SIZE : 0.0f, | ||
| 952 | ImVec2(-1.0f, 0.0f)); | ||
| 953 | ImGui::TextWrapped( | ||
| 954 | "A backup contains the keys that already exist. The key pool keeps " | ||
| 955 | "future mining rewards and receive addresses inside the next backup " | ||
| 956 | "window, but it is not a seed phrase. Back up again after heavy use."); | ||
| 957 | |||
| 958 | ImGui::Spacing(); | ||
| 959 | ImGui::Separator(); | ||
| 960 | if (ImGui::Button("Close", ImVec2(90.0f, 0.0f))) | ||
| 961 | g_showWalletSafety = false; | ||
| 962 | } | ||
| 963 | ImGui::End(); | ||
| 964 | } | ||
| 965 | |||
| 966 | // --------------------------------------------------------------------------- | ||
| 825 | // About dialog | 967 | // About dialog |
| 826 | // --------------------------------------------------------------------------- | 968 | // --------------------------------------------------------------------------- |
| 827 | // What the node knows about itself, in the words it writes to the log. Same | 969 | // What the node knows about itself, in the words it writes to the log. Same |
| @@ -965,6 +1107,7 @@ int RunGUI(int argc, char* argv[]) | |||
| 965 | DrawMainWindow(); | 1107 | DrawMainWindow(); |
| 966 | DrawSendDialog(); | 1108 | DrawSendDialog(); |
| 967 | DrawOptionsDialog(); | 1109 | DrawOptionsDialog(); |
| 1110 | DrawWalletSafetyDialog(); | ||
| 968 | DrawDiagnosticsDialog(); | 1111 | DrawDiagnosticsDialog(); |
| 969 | DrawAboutDialog(); | 1112 | DrawAboutDialog(); |
| 970 | DrawStatusBar(); | 1113 | DrawStatusBar(); |