From 6ca1a1d6799474f8ca977b93ab630af7ace879c2 Mon Sep 17 00:00:00 2001 From: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:45:28 -0400 Subject: Add Linux GUI installer via Wine (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery --------- Co-authored-by: Bitflash Pool Dev --- src/gui.cpp | 766 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 src/gui.cpp (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 0000000..6d81b9f --- /dev/null +++ b/src/gui.cpp @@ -0,0 +1,766 @@ +// Bitflash GUI — ImGui + GLFW + OpenGL3 +// Same source on Linux and Windows. No platform ifdefs. + +#include "imgui/imgui.h" +#include "imgui/imgui_impl_glfw.h" +#include "imgui/imgui_impl_opengl3.h" +#include +#include "headers_core.h" +#include +#include +#include +#include +#include +#include +#include "font_roboto.h" + +// --------------------------------------------------------------------------- +// Node API +// --------------------------------------------------------------------------- +extern int64 nTransactionFee; +extern int fGenerateBitcoins; +extern int nBestHeight; +extern int nMineMode; +extern std::string strParticipantPool; +extern CAddress addrLocalHost; + +void MainFrameRepaint(); +string DateTimeStr(int64 nTime); +bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); +int64 GetBalance(); +string PubKeyToAddress(const std::vector& vchPubKey); +bool AddressToHash160(const std::string& str, uint160& hash160Ret); +void ThreadBitcoinMiner(void*); +void ThreadRPCServer(void*); + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- +static std::string g_myAddress; +static int64 g_balance = 0; +static int64 g_lastWalletRefresh = 0; +static bool g_showSend = false; +static bool g_showOptions = false; +static bool g_showAbout = false; +static bool g_needRefresh = true; + +static char g_sendAddr[128] = {}; +static char g_sendAmount[32] = {}; +static std::string g_sendStatus; +static char g_participantPool[256] = {}; +static char g_poolName[128] = {}; +static char g_poolFee[32] = {}; +static char g_poolDash[256] = {}; +static int g_mineRadio = 0; + +struct TxRow { std::string date, desc; int64 amount; int depth; }; +static std::vector g_txRows; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static std::string FmtMoney(int64 n) +{ + bool neg = n < 0; if (neg) n = -n; + char buf[64]; + snprintf(buf, sizeof(buf), "%s%lld.%08lld", + neg?"-":"", (long long)(n/COIN), (long long)(n%COIN)); + char* dot = strchr(buf, '.'); + if (dot) { char* e = buf+strlen(buf)-1; while(e>dot+2&&*e=='0') *e--='\0'; } + return buf; +} + +static std::string FmtAge(int64 createdAt) +{ + int64 age = GetTime() - createdAt; + if (age < 0) + age = 0; + if (age < 60) + return strprintf("%llds", (long long)age); + if (age < 3600) + return strprintf("%lldm", (long long)(age / 60)); + return strprintf("%lldh", (long long)(age / 3600)); +} + +static ImVec4 ParticipantStatusColor(const std::string& status) +{ + if (status.find("failed") != std::string::npos || + status.find("rejected") != std::string::npos || + status.find("stopped") != std::string::npos) + return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); + if (status.find("hashing") != std::string::npos || + status.find("accepted") != std::string::npos || + status.find("connected") != std::string::npos) + return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); + if (status.find("asking") != std::string::npos || + status.find("authoriz") != std::string::npos || + status.find("subscrib") != std::string::npos) + return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); + return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); +} + +static std::string PoolLabel(const BtfPoolAnnouncement& ann) +{ + std::ostringstream o; + o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; + if (ann.connectedMiners > 0) + o << " - " << ann.connectedMiners << " miners"; + return o.str(); +} + +static void RefreshWallet() +{ + std::vector vchPubKey; + if (CWalletDB("r").ReadDefaultKey(vchPubKey)) + g_myAddress = PubKeyToAddress(vchPubKey); + g_balance = GetBalance(); + + g_txRows.clear(); + TRY_CRITICAL_BLOCK(cs_mapWallet) + { + std::vector> vs; + for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first}); + std::sort(vs.rbegin(), vs.rend()); + for (auto& sv : vs) { + auto mi = mapWallet.find(sv.second); + if (mi == mapWallet.end()) continue; + CWalletTx& wtx = mi->second; + int64 net = wtx.GetCredit() - wtx.GetDebit(); + int depth = wtx.GetDepthInMainChain(); + std::string desc; + if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated"; + else if (wtx.GetDebit() > 0) desc = "Sent"; + else desc = "Received"; + g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); + } + } + g_lastWalletRefresh = GetTime(); +} + +void MainFrameRepaint() { g_needRefresh = true; } + +string DateTimeStr(int64 nTime) +{ + time_t t = (time_t)nTime; + struct tm* p = localtime(&t); + if (!p) return ""; + char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); + return buf; +} + +// --------------------------------------------------------------------------- +// Main window +// --------------------------------------------------------------------------- +static void DrawMainWindow() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowPos(ImVec2(0,0)); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12)); + ImGui::Begin("##main", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar | + ImGuiWindowFlags_NoFocusOnAppearing); + ImGui::SetWindowFontScale(0.92f); + ImGui::PopStyleVar(); + + // ---- Menu bar ---- + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Options")) g_showOptions = true; + if (ImGui::MenuItem("About")) g_showAbout = true; + ImGui::EndMenuBar(); + } + + // ---- Address bar ---- + ImGui::Spacing(); + ImGui::TextDisabled("Address:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-80); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1)); + ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, + ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); + + // ---- Balance on its own line ---- + ImGui::TextDisabled("Balance:"); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f)); + ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); + ImGui::PopStyleColor(); + + // ---- Toolbar ---- + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1)); + if (ImGui::Button(" Send Coins ")) g_showSend = true; + ImGui::PopStyleColor(2); + + // Mining status indicator + ImGui::SameLine(0, 20); + if (fGenerateBitcoins) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f)); + const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" : + nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining"; + ImGui::Text("⬤ %s", modeLabel); + ImGui::PopStyleColor(); + if (nMineMode == MINE_PARTICIPANT) { + std::string mineStatus = GetParticipantMiningStatus(); + if (!mineStatus.empty()) { + ImGui::SameLine(0, 12); + ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus)); + ImGui::Text("[%s]", mineStatus.c_str()); + ImGui::PopStyleColor(); + } + } + } else { + ImGui::TextDisabled("○ Not mining"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // ---- Live telemetry strip ---- + { + const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" : + nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo"; + int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1; + if (age < 0) age = 0; + + ImGui::Text("Mode: %s", modeName); + ImGui::SameLine(0, 16); + ImGui::Text("Wallet refresh: %llds ago", (long long)age); + + if (nMineMode == MINE_PARTICIPANT) { + uint64 sent = 0, accepted = 0; + double hashRate = 0.0; + std::string mineStatus = GetParticipantMiningStatus(); + GetParticipantMiningStats(sent, accepted, hashRate); + if (sent > 0 || accepted > 0 || hashRate > 0.0) { + double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; + ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s", + (unsigned long long)sent, + (unsigned long long)accepted, + acceptPct, + hashRate); + } else if (!mineStatus.empty()) { + ImGui::TextDisabled("Your mining: %s", mineStatus.c_str()); + } else { + ImGui::TextDisabled("Your mining: asking pool for work"); + } + } else if (nMineMode == MINE_OPERATOR) { + int miners = 0, blocksFound = 0; + uint64 roundShares = 0; + GetPoolOperatorStats(miners, blocksFound, roundShares); + if (miners > 0 || blocksFound > 0 || roundShares > 0) + ImGui::TextDisabled("Pool runtime: %d authorized miners, %llu round shares, %d blocks found", miners, (unsigned long long)roundShares, blocksFound); + else + ImGui::TextDisabled("Pool runtime: waiting for miners"); + } else { + ImGui::TextDisabled("Solo runtime: local wallet mining telemetry"); + } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + } + + // ---- Transaction list ---- + float listHeight = io.DisplaySize.y * 0.26f; + if (listHeight < 190.0f) listHeight = 190.0f; + if (listHeight > 260.0f) listHeight = 260.0f; + if (ImGui::BeginTable("txlist", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, listHeight))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110); + ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130); + ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140); + ImGui::TableHeadersRow(); + + for (auto& row : g_txRows) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (row.depth < 1) + ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed"); + else + ImGui::Text("%d blocks deep", row.depth); + ImGui::TableSetColumnIndex(1); + ImGui::TextDisabled("%s", row.date.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::TextUnformatted(row.desc.c_str()); + ImGui::TableSetColumnIndex(3); + if (row.amount >= 0) + ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str()); + else + ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str()); + } + ImGui::EndTable(); + } + + std::vector pools; + BtfGetPoolAnnouncements(pools); + + if (nMineMode == MINE_PARTICIPANT) { + ImGui::Spacing(); + ImGui::SeparatorText("Participant Mining"); + ImGui::Text("Selected pool:"); + if (strParticipantPool.empty()) + ImGui::TextDisabled("No pool selected yet"); + else + ImGui::TextWrapped("%s", strParticipantPool.c_str()); + + const BtfPoolAnnouncement* selected = NULL; + for (size_t i = 0; i < pools.size(); i++) + if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; } + + if (selected) { + ImGui::Text("Pool name: %s", selected->poolName.c_str()); + ImGui::Text("Fee: %.2f%%", selected->feePercent); + ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound); + if (!selected->dashboardUrl.empty()) + ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str()); + } else if (!strParticipantPool.empty()) { + ImGui::TextDisabled("No fresh announcement found for this pool yet"); + } + + if (!pools.empty()) { + ImGui::Spacing(); + ImGui::SeparatorText("Live Pools"); + if (ImGui::BeginTable("livepools", 5, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70); + ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160); + ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + for (size_t i = 0; i < pools.size(); i++) { + const BtfPoolAnnouncement& ann = pools[i]; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(ann.poolName.c_str()); + ImGui::TextDisabled("%s", ann.btfAddress.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.2f%%", ann.feePercent); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str()); + ImGui::TableSetColumnIndex(3); + if (!ann.dashboardUrl.empty()) + ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); + else + ImGui::TextDisabled("-"); + ImGui::TableSetColumnIndex(4); + if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) { + strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); + strParticipantPool = ann.btfAddress; + CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); + g_needRefresh = true; + } + } + ImGui::EndTable(); + } + } + } + + if (nMineMode == MINE_OPERATOR) { + ImGui::Spacing(); + ImGui::SeparatorText("Operator Overview"); + ImGui::Text("Pool .btf address:"); + std::string opAddr = BtfLocalAddress(); + if (opAddr.empty()) + ImGui::TextDisabled("Identity not ready yet"); + else + ImGui::TextWrapped("%s", opAddr.c_str()); + + std::vector owed; + GetPendingPayouts(owed); + ImGui::Spacing(); + ImGui::SeparatorText("Pending Pool Payouts (you owe miners)"); + if (owed.empty()) { + ImGui::TextDisabled("No pending pool payouts"); + } else if (ImGui::BeginTable("owedpayouts", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + for (const PendingPayoutView& row : owed) { + int remaining = std::max(0, row.matureAtHeight - nBestHeight); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%d", row.matureAtHeight); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%d", remaining); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%d", row.recipients); + ImGui::TableSetColumnIndex(3); + ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); + } + ImGui::EndTable(); + } + + std::vector workers; + GetPoolWorkerStats(workers); + ImGui::Spacing(); + ImGui::SeparatorText("Current Round Workers"); + if (workers.empty()) { + ImGui::TextDisabled("No active workers in this round"); + } else if (ImGui::BeginTable("workers", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableHeadersRow(); + for (const PoolWorkerStatView& row : workers) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(row.address.empty() ? "unknown" : row.address.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%llu", (unsigned long long)row.roundShares); + ImGui::TableSetColumnIndex(3); + ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str()); + } + ImGui::EndTable(); + } + } + + // ---- Status bar ---- + { + ImGui::Spacing(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,2)); + + int peers = (int)vNodes.size(); + if (nMineMode == MINE_PARTICIPANT) { + uint64 sent = 0, accepted = 0; + double hashRate = 0.0; + std::string mineStatus = GetParticipantMiningStatus(); + GetParticipantMiningStats(sent, accepted, hashRate); + if (sent > 0 || accepted > 0 || hashRate > 0.0) + ImGui::TextDisabled("Peers %d height %d shares sent %llu accepted %llu hash rate %.2f H/s", + peers, + nBestHeight, + (unsigned long long)sent, + (unsigned long long)accepted, + hashRate); + else if (!mineStatus.empty()) + ImGui::TextDisabled("Peers %d height %d %s", peers, nBestHeight, mineStatus.c_str()); + else + ImGui::TextDisabled("Peers %d height %d asking pool for work", peers, nBestHeight); + } else if (nMineMode == MINE_OPERATOR) { + int miners = 0, blocksFound = 0; + uint64 roundShares = 0; + GetPoolOperatorStats(miners, blocksFound, roundShares); + if (miners > 0 || blocksFound > 0 || roundShares > 0) + ImGui::TextDisabled("Peers %d height %d authorized miners %d round shares %llu blocks found %d", + peers, nBestHeight, miners, (unsigned long long)roundShares, blocksFound); + else + ImGui::TextDisabled("Peers %d height %d waiting for miners", peers, nBestHeight); + } else { + ImGui::TextDisabled("Peers %d height %d", peers, nBestHeight); + } + ImGui::PopStyleVar(); + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// Send dialog +// --------------------------------------------------------------------------- +static void DrawSendDialog() +{ + if (!g_showSend) return; + ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + if (ImGui::Begin("Send Coins", &g_showSend, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::Text("Recipient address:"); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); + + ImGui::Text("Amount (BTF):"); + ImGui::SetNextItemWidth(180); + ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); + + if (!g_sendStatus.empty()) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str()); + } + + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); + if (ImGui::Button("Send", ImVec2(90,0))) { + uint160 h; int64 nv = 0; + if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; + else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount."; + else { + CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; + CWalletTx wtx; + if (SendMoney(s, nv, wtx)) { + g_showSend=false; g_sendStatus=""; + memset(g_sendAddr,0,sizeof(g_sendAddr)); + memset(g_sendAmount,0,sizeof(g_sendAmount)); + g_needRefresh = true; + } else g_sendStatus = "Transaction failed."; + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; } + } + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// Options dialog +// --------------------------------------------------------------------------- +static void DrawOptionsDialog() +{ + if (!g_showOptions) return; + + // Always sync from current globals when dialog opens + static bool wasOpen = false; + if (!wasOpen) { + g_mineRadio = nMineMode; + strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + strncpy(g_poolName, strPoolName.c_str(), sizeof(g_poolName)-1); + snprintf(g_poolFee, sizeof(g_poolFee), "%.2f", dPoolFeePercent); + strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); + } + wasOpen = true; + ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + if (ImGui::Begin("Options", &g_showOptions, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + // Transaction fee + ImGui::SeparatorText("Transaction Fee"); + static char feeStr[32] = {}; + if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str()); + ImGui::SetNextItemWidth(180); + ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); + + // Mining + ImGui::SeparatorText("Mining Mode"); + ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO); + ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR); + ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); + + if (g_mineRadio == MINE_OPERATOR) { + ImGui::Spacing(); + ImGui::SeparatorText("Pool Announcement"); + ImGui::Text("Pool name:"); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); + ImGui::Text("Fee %%:"); + ImGui::SetNextItemWidth(120); + ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); + ImGui::Text("Dashboard URL:"); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); + } + + if (g_mineRadio == MINE_PARTICIPANT) { + ImGui::Spacing(); + ImGui::SeparatorText("Pool Selection"); + ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here."); + + std::vector pools; + BtfGetPoolAnnouncements(pools); + if (!pools.empty()) { + std::string current = "Select a live pool"; + for (size_t i = 0; i < pools.size(); i++) { + if (pools[i].btfAddress == g_participantPool) { + current = PoolLabel(pools[i]); + break; + } + } + if (ImGui::BeginCombo("Discovered pools", current.c_str())) { + for (size_t i = 0; i < pools.size(); i++) { + const BtfPoolAnnouncement& ann = pools[i]; + std::string label = PoolLabel(ann); + bool selected = ann.btfAddress == g_participantPool; + if (ImGui::Selectable(label.c_str(), selected)) { + strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); + } + } + ImGui::EndCombo(); + } + } else { + ImGui::TextDisabled("No live pools discovered yet"); + } + } + + ImGui::Spacing(); + const char* genLabel = fGenerateBitcoins ? "Stop Mining" : "Start Mining"; + if (ImGui::Button(genLabel, ImVec2(130,0))) { + fGenerateBitcoins = fGenerateBitcoins ? 0 : 1; + if (fGenerateBitcoins) _beginthread(ThreadBitcoinMiner, 0, NULL); + CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); + } + + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); + if (ImGui::Button("OK", ImVec2(90,0))) { + int64 fee=0; + if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); } + + bool changed = (g_mineRadio != nMineMode); + nMineMode = g_mineRadio; + strParticipantPool = g_participantPool; + strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; + strPoolDashboardUrl = g_poolDash; + dPoolFeePercent = atof(g_poolFee); + CWalletDB().WriteSetting("nMineMode", nMineMode); + CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); + CWalletDB().WriteSetting("strPoolName", strPoolName); + CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl); + CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent); + + if (nMineMode == MINE_OPERATOR) { + if (!gPoolServerRunning) { + gPoolServerRunning = true; + _beginthread(ThreadRPCServer, 0, NULL); + } else if (!gPoolRunning) { + gPoolRunning = true; + } + } else { + gPoolRunning = false; + } + g_showOptions = false; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false; + } + if (!g_showOptions) wasOpen = false; + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// About dialog +// --------------------------------------------------------------------------- +static void DrawAboutDialog() +{ + if (!g_showAbout) return; + ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + if (ImGui::Begin("About Bitflash", &g_showAbout, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0"); + ImGui::Spacing(); + ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine."); + ImGui::Spacing(); + ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); + ImGui::Spacing(); + if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false; + } + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- +int RunGUI(int argc, char* argv[]) +{ + if (!glfwInit()) return 1; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + + GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr); + if (!window) { glfwTerminate(); return 1; } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; + + // Load Roboto at 16px — clean, readable on all platforms + io.Fonts->AddFontFromMemoryCompressedTTF( + RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f); + + ImGui::StyleColorsDark(); + ImGuiStyle& s = ImGui::GetStyle(); + s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f; + s.ItemSpacing = ImVec2(10, 7); + s.FramePadding = ImVec2(8, 5); + s.WindowPadding = ImVec2(12, 12); + // Orange accent + s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1); + s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1); + s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1); + s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f); + s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f); + s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1); + s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1); + s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1); + s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1); + s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1); + s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1); + s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1); + + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init("#version 330"); + + g_mineRadio = nMineMode; + strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + + int frame = 0; + while (!glfwWindowShouldClose(window) && !fShutdown) + { + glfwPollEvents(); + if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } + frame++; + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + DrawMainWindow(); + DrawSendDialog(); + DrawOptionsDialog(); + DrawAboutDialog(); + + ImGui::Render(); + int w, h; glfwGetFramebufferSize(window, &w, &h); + glViewport(0, 0, w, h); + glClearColor(.08f,.08f,.08f,1); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(window); + } + + fShutdown = true; + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + glfwDestroyWindow(window); + glfwTerminate(); + return 0; +} -- cgit v1.2.3 From ebb793b0c513e5ee0298228b0185b4bada842513 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 28 Jul 2026 12:25:01 -0300 Subject: GUI: print a clear error when OpenGL 3.3 window creation fails (#7) RunGUI() silently returned 1 and the process exited via StopNode() when glfwInit or glfwCreateWindow failed, which reads like a crash. On machines without OpenGL 3.3 core (RDP sessions, VMs, old GPUs) the window never appeared with no explanation. Print an actionable message pointing to driver update / software OpenGL (Mesa) / running headless with -nogui. --- src/gui.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 6d81b9f..2210296 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -678,7 +678,10 @@ static void DrawAboutDialog() // --------------------------------------------------------------------------- int RunGUI(int argc, char* argv[]) { - if (!glfwInit()) return 1; + if (!glfwInit()) { + printf("GUI ERROR: glfwInit() failed -- no display or GL available. Run headless with -nogui.\n"); + return 1; + } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); @@ -687,7 +690,13 @@ int RunGUI(int argc, char* argv[]) #endif GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr); - if (!window) { glfwTerminate(); return 1; } + if (!window) { + printf("GUI ERROR: could not create an OpenGL 3.3 window.\n"); + printf(" Your GPU/driver may not support OpenGL 3.3 core -- common over Remote Desktop, in VMs, or on old GPUs.\n"); + printf(" Fixes: update graphics drivers, use a software OpenGL (Mesa), or run headless with -nogui.\n"); + glfwTerminate(); + return 1; + } glfwMakeContextCurrent(window); glfwSwapInterval(1); -- cgit v1.2.3 From 663784eae9b04ec71e3e1eebf445910e5a8add42 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 28 Jul 2026 12:55:04 -0300 Subject: Merge PR #6 (large refactor), preserving main's fixes (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery * Stop persisting mining mode/pool settings across restarts LoadWallet() runs after CLI flags are parsed in main_gui.cpp, and was restoring nMineMode/strParticipantPool/strPoolName/strPoolDashboardUrl/ dPoolFeePercent/fGenerateBitcoins from wallet.dat every time, silently overwriting whatever was just set on the command line or in the current Options session -- no error, no log line. This is why pool name/address could show correctly in the UI but stay stale in the logs (UI held the in-memory value just set; logs eventually reflected whatever LoadWallet clobbered it back to), and why mode changed behavior across restarts. Mode/pool config is now decided fresh every launch: CLI flags if given, otherwise the compiled-in default. Nothing is written back to wallet.dat for these settings anymore, so there's nothing left to go stale or fight over load order. * Add explicit Relay mode as the new startup default 4th mining mode: node runs/syncs, never mines, no pool server -- an explicit option instead of an implicit side-effect of 'Solo + never click Start Mining'. Now the compiled-in default (was Solo). - main.h: MINE_RELAY = 0, renumbered other modes (nothing compares nMineMode against raw integers elsewhere, verified). - main.cpp: default nMineMode = MINE_RELAY; BitcoinMiner() early-returns for Relay, mirroring the existing Participant-mode guard. - gui.cpp: added Relay radio button, status label, and telemetry-strip text. Start/Stop Mining button is hidden for Relay (meaningless -- it never mines by definition); OK forces fGenerateBitcoins off if you switch into Relay while previously mining. * Announce pool changes immediately instead of waiting up to 60s ThreadBtfPoolAnnouncer re-read strPoolName/dPoolFeePercent/etc live, but only once per iteration of its 60s sleep loop, so a pool name/fee change in Options could take up to a minute to reach other nodes. - nostr.h/.cpp: add gAnnounceNow flag; the announcer's sleep loop now wakes early and publishes immediately when it's set, then clears it. - gui.cpp: Options OK handler sets gAnnounceNow when operator pool settings actually changed (name/dashboard/fee/mode), so a saved change is announced right away instead of on the next tick. * Give each pool miner a distinct extranonce2 so jobs no longer overlap mining.notify previously broadcast one identical 80-byte header (baked from a single server-side extraNonce) to every connected miner, so any two miners on the same job searched the exact same nNonce space instead of disjoint ranges. Each Miner connection is now assigned a unique extranonce2 on connect. BuildMinerBlock() takes the shared job template and appends that miner's extranonce2 to the coinbase scriptSig, recomputing the merkle root, so every miner gets its own header variant of the same job. mining.submit validates and, on a full block, assembles the block using that same miner-specific variant (via m->extranonce2), not the shared template. No client-side protocol change needed: main.cpp's participant miner already just takes whatever header bytes mining.notify hands it and submits (jobId, nonce) back, so it works unmodified against per-miner headers. Still simplified vs standard Stratum (no coinb1/coinb2/merkle-branch wire split, no mining.extranonce.subscribe reply payload) since this protocol was already a custom simplification pre-existing this fix. Syntax-checked with g++ -fsyntax-only (full link needs secp256k1/ RandomX/GLFW not present in this sandbox). * Fix debug.log never being written on the real Windows build printf-to-debug.log routing was gated on: #if defined(_WINDOWS) || !defined(_WIN32) _WINDOWS is only ever defined by the legacy, unused MSVC makefile.vc. The actual Windows build (makefile.mingw) never defines it, and _WIN32 IS defined there (mingw builtin), so on real Windows this condition was always false || false = false. printf() fell through to plain libc printf (stdout only), so debug.log was never created, and output was only visible if the process happened to be run attached to a console. OutputDebugStringF() was already platform-safe internally (always writes debug.log; only echoes to console when not on Windows), so the outer condition was pure dead weight. Made the routing unconditional. * Reduce base UI font size one more step (16px -> 14px) * Remove Start/Stop Mining button; mining state follows selected mode The button let a user configure a pool then click a separate toggle, creating states where the button's label/action didn't match what the selected mode actually needed. OK now starts/stops the miner thread itself based on nMineMode: Relay clears fGenerateBitcoins, the other three modes set it and spawn ThreadBitcoinMiner if it isn't already running (guarded by vfThreadRunning[3]). * Fix participant miner stuck waiting for notify after buffered multi-line recv Root cause of the operator/participant pairing successfully but no work ever flowing: PoolParticipantMiner()'s receive loop only pulled one line out of the socket per select() wakeup. The server sends the authorize reply, mining.set_difficulty, and mining.notify back-to-back on the same connection, and these often land in a single TCP segment — one recv() inside StratumRecvLine() buffers all three lines into 'buf', but only the first was extracted and processed. The remaining lines (frequently mining.notify itself) sat in 'buf' indefinitely, because select() only reports readability of the OS socket, not whether our own buffer still holds unprocessed complete lines — and the server had nothing further to send once the initial burst was over, so select() never fired again. Fix: drain any complete line already sitting in 'buf' first; only fall back to select()+recv() when 'buf' has no complete line left. * Fix mining.submit rejecting every share due to wrong param-count check Server required params.size() >= 4 for mining.submit, but the client only ever sends 3: [username, jobId, nonceHex] (indices 0-2, matching what the handler actually reads: p[1]=jobId, p[2]=nonceHex). Every submission was bounced with 'bad params' before its nonce was ever checked against the job, producing an endless share-sent/share-rejected loop with no other visible cause. Likely a leftover from the standard 5-field Stratum mining.submit shape, which this simplified 3-field protocol never matched. Threshold corrected to size() >= 3. * Fix mining.submit rejecting legitimate shares that weren't also winning blocks Pool share difficulty is intentionally ~256x easier than full network difficulty (MakeShareTarget() shifts the block target left 8 bits), so miners are expected to submit many shares per actual block found. But the handler replied to the miner based on ProcessBlock()/CheckBlock() success — validation against the much harder network target — rather than on whether the share met pool difficulty. Every legitimate share that wasn't also a winning block (the vast majority, by design) failed CheckBlock()'s nBits check and got reported to the miner as rejected. Now: reply(true) as soon as a submission clears pool-share difficulty. Only if it ALSO clears full network difficulty (job.target, not job.shareTarget) does it get submitted as a block — with no second reply sent, since the share was already acknowledged. * Fix dead-peer dial priority: freshness sort was undone by full shuffle ConnectDiscoveredBtfPeers sorted candidates newest-first, then immediately shuffled the whole list, making the freshness sort a no-op — every pass was fully random. Meanwhile g_btfPeerFails was tracked but never used to prioritize, so a peer that had failed 20 times in a row got equal footing with an untried or previously-successful peer; the only effect was backoff, capped at 15 minutes, so dead descriptors cycled back into the dial pool constantly. - Track successful connections (g_btfPeerEverConnected) and use it, plus fail history, to order candidates in tiers: known-good, then untried, then known-bad (worst offenders last). Freshness order is preserved within each tier instead of being discarded. - Raise the backoff cap for chronic failures (6+ in a row) from 15 minutes to 6 hours, so descriptors that are clearly dead stop consuming dial budget every pass while still getting retried eventually. * Batch-resolve .btf descriptors to stop hammering public relays during dial passes Root cause of 'stuck at 1 peer' despite good peer discovery: each parallel dial thread in ConnectDiscoveredBtfPeers called ConnectNodeBtf, which itself walked all configured relays (relay.damus.io, nos.lol, relay.nostr.band, relay.snort.social) to resolve that one peer's descriptor. With up to BTF_DIALS_PER_PASS (12) threads dialing at once, a single pass could fire up to 12 * nNostrRelays near-simultaneous fresh connections at the same handful of public relays -- easily enough to trip rate limits or get handshakes refused (exactly what the logs showed: 'handshake refused by relay.damus.io', repeated connect failures to relay.nostr.band / relay.snort.social). Peers that were genuinely reachable looked dead simply because their descriptor couldn't be fetched under that load, and then got penalized as known-bad by the dial-priority tiering from the previous fix. - Add BtfResolveMany: resolves every still-unresolved candidate's descriptor in a single Nostr REQ per relay (multiple authors in one filter), instead of one relay connection per candidate. A 12-candidate pass now costs at most a few relay round-trips total instead of dozens. - Add ConnectNodeBtfResolved / ConnectNodeBtfTail (net.cpp): given an already-resolved descriptor, dial straight to the peer's rendezvous node with zero further relay contact. ConnectDiscoveredBtfPeers now batch- resolves all candidates up front, then fans out tunnel-only connect threads in parallel -- no thundering herd against the relays. - Candidates that fail to resolve are recorded as a failure immediately (same backoff/fail-count bookkeeping as before) instead of spawning a thread with nothing to dial. * Stop reconnecting to relays constantly: pool one persistent connection per relay Real root cause of the ongoing 'handshake refused' / 'connect failed' spam and peer count stuck at 0-1: every relay-touching operation (startup self- test, per-pass seeding, batch descriptor resolve, pool announce, pool query) opened its own brand-new CWebSocket every single time it ran. Two independent threads (ThreadNostrSeed and ThreadBtfPoolAnnouncer) each reconnect from scratch on their own ~60s cycle, so a node could fire well over a dozen fresh TCP+TLS+WS handshakes at the same 4 public relays inside any given minute. Public relays -- relay.damus.io especially -- rate-limit new connections per source IP fairly aggressively, so this node was effectively DoS-ing itself: peers were genuinely reachable, but their descriptors couldn't be fetched because the relay had already started refusing this node's own repeated reconnects. - Add a small per-relay connection pool (g_relayPool) and an RAII CRelayLease that reuses a relay's live socket across calls instead of reconnecting every time, only reconnecting when a socket actually drops or was never established. - Route every relay call site through it: the startup self-test, the self-test loop, SeedFromRelay, BtfResolve, BtfResolveMany, BtfPublishPoolAnnouncement, BtfQueryPoolAnnouncement. - Only log 'connected to ' on an actual fresh (re)connect, not on every pass that's just reusing an already-live socket, so the log reflects what's really happening instead of implying constant churn. This cuts fresh relay connections from several per minute down to essentially one per relay for the life of the process (until a relay actually drops the connection), which should make the rate-limit-driven refusals go away and let descriptor resolution -- and therefore dialing -- actually succeed at the rate the peer list implies it should. * Fix wallet crash on rejected send; remove legacy IP-based peer discovery - SendMoney() no longer throws when a committed tx is rejected by AcceptTransaction() (e.g. raced input already spent). It now unwinds the wallet state via RollbackTransactionSpent() and returns false, which the GUI already handles as 'Transaction failed.' - Removed the dead IP-based peer discovery/connect path now that discovery is entirely Nostr/.btf: CAddrDB/addr.dat, mapAddresses, AddAddress, ConnectNode(CAddress), FindNode(CAddress), ConnectSocket (SOCKS4 proxy helper), addrProxy, ThreadOpenConnections/2 class-C dialer and its thread spawn, the addr/getaddr P2P handlers, and the dead vAddrToSend/setAddrKnown fields on CNode. - Deleted irc.cpp/irc.h (already-disabled legacy seed code) and scrubbed references from headers_core.h and all four makefiles. * Raise peer connection target and add dial diversity; fix IPv4-only relay connect - BTF_TARGET_CONN: 8 -> 24. With ~100 known .btf peers, average degree needs only to exceed ln(N)~=4.6 for the network to stay connected with overwhelming probability (Erdos-Renyi); 24 is a healthy margin. - ConnectDiscoveredBtfPeers now splits each pass's dial budget ~70/30 between the existing reliability-tiered order and a random sample from the full fresh peer pool. Pure reliability bias means every node converges on the same 'known good' hub set, which correlates peer sets across the network -- a relay outage then drops the same edges everywhere at once instead of independent ones. The random slice keeps a floor of uncorrelated edges. - CWebSocket::Connect (relay websocket) now resolves with AF_UNSPEC and tries every returned address (IPv6 or IPv4) in turn instead of hard-failing on AF_INET-only resolution, which was one plausible cause of the relay.nostr.band:443 connect failures (CDN-fronted relays increasingly rate-limit/filter the v4 path). * Remove accidentally committed build binary; ignore src/bitflash * Fix pool participants getting stuck after the first job JobWatcherFn only rebuilt/broadcast a new Stratum job when a new block arrived on this node or no job existed yet. Once a job was built after startup, gHaveJob stayed true forever unless *this* node found the block, so with no new block (slow interval, or this node just not seeing one) every connected miner sat hashing the exact same job -- stale timestamp, stale mempool snapshot -- indefinitely. From the miner's side that's indistinguishable from being stuck: it worked at connect (gets the one-time job sent on authorize) and then nothing ever arrives again. Added a 60s periodic refresh so the job (and therefore mining.notify) gets rebroadcast on a timer too, same as a normal stratum pool, not only on new-block/no-job events. * Diversify Nostr discovery relay list Went from 4 relays to 7, and reduced Cloudflare concentration: previously 2 of 4 (damus.io, snort.social) sat behind the same CDN, so a Cloudflare- side issue could take out half the list even though they're nominally independent relays. Kept damus.io (most established) as the one CF-fronted entry and filled the rest with different hosts (nos.lol, nostr.band on Hetzner, nostr21.com, offchain.pub, nostr.mom, primal.net) so peer discovery doesn't have a single shared upstream as a point of failure. 7 relays is within the commonly recommended 5-10 range for redundancy. * Fix data race in RebuildJob: chain-state reads weren't holding cs_main pindexBest, nBestHeight, GetNextWorkRequired() and GetMedianTimePast() were all read without cs_main. A block being accepted concurrently by the P2P thread could interleave with this: pindexPrev captured from the old best block, but nBestHeight already bumped to the new one by the time job.height is computed, or pindexBest reassigned mid-read. That mismatch would make QueuePayouts() schedule a payout for the wrong maturity height for whatever block gets found against that job. Wrapped the whole chain-state snapshot in one CRITICAL_BLOCK(cs_main), consistent with how the rest of the codebase touches this state. * add operator fee * Add categorized, burst-collapsing logging for pool/worker/payout flow debug.log was growing unboundedly because every printf() call wrote unconditionally, so loops that log once per item (discovered peers, connect attempts) produced a near-identical line per address with no cap. Add LogPrint(category, ...): a drop-in printf replacement that tags each call with a subsystem (pool/worker/payout/net/nostr) and collapses repeated calls from the same call site into one line per 3s window plus a '(xN similar in 3s)' summary, so log growth is bounded by elapsed time instead of event count. Wire it through the whole pool -> work -> payout pipeline (rpc.cpp operator side, main.cpp participant/worker side, net.cpp/nostr.cpp peer & relay discovery), and along the way fix several previously *silent* failure points that produced exactly the reported symptom (worker stuck asking for work, nothing logged): - Relay listener registration failing: no log at all, so a pool with every relay down looked identical to a healthy idle pool. - No relays configured: silent infinite 5s retry loop. - Initial job seed failing at pool startup: silent. - JobWatcher: added a stuck-job watchdog that, after 15s with no job available, logs a specific diagnosis (no chain tip yet vs. RebuildJob() failing) instead of retrying forever in silence. - QueuePayouts(): a block found with zero recorded shares queued no payout and logged nothing. - FlushMaturePayouts(): an unparseable payout address was skipped with no log. - Participant miner: the 'waiting for pool notify' stall was only ever shown as a transient GUI status string, never written to debug.log. Now logs on a 15s cadence while stalled, and after 120s proactively drops the tunnel so the existing reconnect-in-10s loop gets a fresh attempt instead of hanging indefinitely. - Participant miner: share rejections were only a transient GUI status string; now logged with the pool's rejection reason. All edited files verified with g++ -fsyntax-only (gnu++14). * Fix two bugs causing worker stuck + log bloat 1. Worker stuck asking for work (main.cpp): The participant miner checked msg["result"].is_boolean() to detect the mining.subscribe acknowledgement (id==1), but the pool sends an array ([[["mining.notify",sid],sid,4]]) as per Stratum spec, not a boolean. is_boolean() always returned false, so gotSubscribeAck was never set true. This didn't directly block authorize (id==2 path is independent), but it did mean the status never advanced through "subscribed", leaving the progress logging misleading. More importantly, the same wrong assumption meant any pool that also gated job dispatch on a prior subscribe confirmation would leave the worker stuck forever with no diagnosis. Fix: accept any non-null result for id==1 (the array is the session/extranonce info; its contents don't matter for our use case). 2. Log bloat from ProcessMessages (main.cpp): ProcessMessages() used a bare printf() which fired unconditionally for every P2P message received -- the bulk of debug.log was this one line repeating thousands of times. Convert to LogPrint("net", ...) so it is gated behind fDebug (off by default) and burst-collapsed by the dedup introduced in the previous commit. * Fix log bloat, em-dashes, dead code, status bar, Connected/Detected peers Log bloat: - Gate all per-message/per-block/per-loop printf in main.cpp, net.h, net.cpp behind LogAcceptsCategory("net") -- only visible with -debug. This includes: received:/sending: hex dumps, got inventory, askfor, getblocks, getdata, received block, AddToWallet, RelayWalletTransactions, ProcessMessage FAILED, mining loop N-transactions line, retarget block, socket recv/send errors. AddToBlockIndex new best and ProcessBlock ACCEPTED intentionally left always-on (one line per block). - PoolParticipantMiner: status -> now LogPrint(worker) not printf. Em-dashes: - Replace all UTF-8 em-dashes (0xE2 0x80 0x94) with ASCII '--' in every source file (rpc.cpp, gui.cpp, main.cpp, main_gui.cpp, net.cpp, nostr.cpp, util.cpp, btfrv.cpp, db.cpp, db.h, main.h, headers.h, headers_core.h, util.h). 40 occurrences across 14 files. Dead code: - Remove unused gJobSeq atomic from rpc.cpp (written, never read). - Fix minerReward: was computed but payout loop used blockReward directly (operator fee was advertised but never deducted). Now payout correctly uses minerReward = blockReward * (1 - feeFrac). Status bar / peers: - Add GetDiscoveredPeerCount() to nostr.cpp/nostr.h (reads g_btfPeers size under cs_btfPeers). - Replace scrollable inline status text with DrawStatusBar(): a separate ImGui overlay window pinned to bottom of screen (NoScrollbar, NoBringToDisplayFront). Labels changed from 'Peers N' to 'Connected peers: N Detected peers: N'. * Rewrite gui.cpp: proper fixed status bar, scrollable content child Layout architecture: - DrawMainWindow() covers the full display width, height minus STATUS_BAR_H (22px). NoScrollbar + NoScrollWithMouse on the outer window so the chrome (menu, address/balance strip, telemetry strip, separators) never scrolls. - A BeginChild("##scroll") fills the remaining height after the fixed chrome, sized to GetContentRegionAvail().y -- which is already the space between the cursor and the bottom of the outer window. All mode-specific content (transaction table, participant pool list, operator workers/payouts) lives inside this child. Content that overflows gets a scrollbar; nothing bleeds past the outer window edge. - DrawStatusBar() is a separate overlay window pinned to the bottom 22px, drawn last so it is always on top. NoBringToDisplayFront is NOT used here -- drawing last is sufficient and avoids interaction issues with dialogs. - Status bar background is fully opaque (not alpha-blended) so it never shows content behind it. Labels and content: - 'Peers N' replaced with 'Connected peers: N Detected peers: N' using GetDiscoveredPeerCount(). - Mining indicator: '* Mode' instead of the 'filled circle' Unicode glyph that renders as boxes on some Linux font configs. - FmtAge used consistently; 'blocks deep' shortened to 'N deep' in tx list. - Options dialog: em-dashes removed from RadioButton labels (ASCII '--'). - Removed usedY dead variable (GetCursorPosY() result was unused). * Fix blocking bugs, dead code, pool/participant flow correctness Bugs fixed: StratumRecvLine blocking (main.cpp): The participant miner's recv loop called recv() with no timeout after select() said the socket was readable. If the server sent a partial line (split across two TCP segments), the second recv() would block indefinitely waiting for the rest -- the classic 'stuck asking for work' hang even though the pool was alive and had sent data. Fix: single non-blocking recv() pass (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete line is in the buffer yet, return false; the outer select() loop retries when more data arrives. BroadcastJob holding mutex during send() (rpc.cpp): BroadcastJob() held gMinersMutex across all SendLine() calls. send() can block if a slow miner's TCP receive buffer is full. With the mutex held, MinerThreadFn could not acquire it to remove a disconnecting miner, causing a deadlock-adjacent stall: broadcast blocked on slow miner, disconnect blocked on broadcast, new jobs not delivered. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd outside the lock. Dead code removed (rpc.cpp): - gRpcUser, gRpcPassword: defined, never read or set by anything. - dHashesPerSec, nHPSTimerStart: leftover Bitcoin 0.1 stubs, unused. - IsInitialBlockDownload(): stub that returned pindexBest==NULL, never called anywhere in this codebase. - struct Share: declared with three fields, never instantiated. - Duplicate comment block before payout section. Other cleanup (main.cpp): - STUCK_NO_JOB_TIMEOUT_SECS moved from inside the loop body to file scope (was 'static const' inside a loop -- correct but confusing). - pblock->print() in solo miner gated behind LogAcceptsCategory("net") -- was unconditionally dumping a full block to stdout on every solve. - ERROR in BitcoinMiner printf converted to LogPrint("net"). * Pool/participant: fix recv blocking, BroadcastJob mutex, listener spawn, escaped em-dash main.cpp -- StratumRecvLine rewrite: The previous version called recv() in a while loop -- if select() said the socket was readable but only a partial line had arrived, the second recv() call would block indefinitely waiting for the rest of the line. This was the root cause of the recurring stuck-worker bug. Rewrite: single non-blocking recv (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete newline is in the buffer yet, return false immediately; the outer 50ms select() loop retries when more data arrives. This means the hash loop always stays responsive regardless of network fragmentation. rpc.cpp -- BroadcastJob mutex held across send(): BroadcastJob held gMinersMutex for the entire duration of sending jobs to all connected miners. send() can block if a slow miner's TCP buffer is full. With the mutex held, MinerThreadFn could not acquire it to clean up a disconnecting miner, stalling the whole broadcast. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd without holding any lock. rpc.cpp -- LaunchThread failure not handled in BtfStratumAcceptOneFn: The replacement listener thread was launched via LaunchThread() which wraps _beginthread() but discarded the return value. If _beginthread fails (thread limit, OOM), the listener count stayed at 1 with no thread behind it -- that relay would get no new listener until the 30s outer loop in BtfStratumAcceptFn fired. Fix: call _beginthread() directly, check the return value, decrement and log on failure. rpc.cpp -- escaped em-dash in log string: BtfStratumAcceptFn had a literal \xe2\x80\x94 in a LogPrint format string that survived the em-dash sweep (it was source-encoded, not a UTF-8 byte in the file). Replaced with '--'. * Rewrite pool server: single event loop, no per-miner threads Replace the old architecture (one thread per connected miner + separate JobWatcher thread + BtfStratumAcceptFn thread with listener count tracking) with a clean two-layer design: Accept layer (one thread per relay, AcceptOneFn): Each relay gets exactly one persistent thread. It loops: calls blocking RvServiceRegister (waits for a miner to knock), then BtfServiceWrap (echan handshake), then pushes the finished plain socket to a small lock-protected queue, then loops immediately to accept the next miner. No listener count tracking. No replacement thread spawning. No races around spawn failure. The relay always has exactly one pending accept. Event loop (PoolEventLoop, runs on the ThreadRPCServer thread): Single thread owns all connected miner sockets. Each tick: 1. Drain the accept queue -- pick up any newly-connected miners. 2. Check for a new block -- rebuild job and broadcast if so. 3. No-job watchdog -- log and retry if job is missing too long. 4. Periodic job refresh -- restamp nTime every 60s. 5. select() over all miner fds with 50ms timeout. 6. For each readable fd: non-blocking RecvLine, drain all complete lines, dispatch each to HandleLine, collect dead fds. 7. Refresh GUI stats snapshot every 2s. Because the event loop is the only writer to gMiners, no mutex is needed on the miner list. The only mutexes remaining are: - gStatsMutex: guards the GUI-visible snapshot (StatsSnapshot, gWorkerStats, gPayoutViews) written by the event loop and read by GUI threads via GetPoolOperatorStats/GetPoolWorkerStats/GetPendingPayouts. - gReadyMutex: guards the accept queue between AcceptOneFn threads and the event loop. Other improvements in this rewrite: - gPendingPayouts is event-loop-only (no mutex needed). - Round share tracking lives in the event loop (roundShareCount map + roundShareTotal counter) -- zeroed on block found, no per-miner stat mutex needed mid-round. - QueuePayouts takes the share snapshot as a parameter instead of reaching into gMinerStats under a separate mutex. - BroadcastJob no longer snapshots miners to avoid a mutex -- it runs directly in the event loop with exclusive access to gMiners. - AcceptOneFn backs off 30s on relay registration failure instead of returning -- one relay going down doesn't kill the accept thread. - No goto. No per-thread dynamic allocation for listener contexts on each accepted connection. * Thorough logging and correctness fixes across the full pool/participant flow Every failure point in the operator->miner->payout chain now logs exactly what broke, with enough context to diagnose without a debugger. Operator side (rpc.cpp): RebuildJob: logs 'no chain tip (pindexBest is null)' on failure instead of silently returning false. HandleLine: - JSON parse errors logged with the raw line (first 80 chars) - subscribe: logs fd and session id - authorize: logs address, worker, and whether the address is valid - submit: logs stale-job case with submitted vs current job id; logs high-hash with actual hash and target values; logs valid shares with nonce and running session count; logs BLOCK CANDIDATE separately from BLOCK ACCEPTED/REJECTED so a ProcessBlock failure is unambiguous - unknown method logged (reveals miner software quirks) - reply() logs send failures with method name QueuePayouts: logs total payout amount, miner count, fee applied, and how many miners were below dust threshold. FlushMaturePayouts: logs 'insufficient funds or wallet locked?' on SendMoney failure instead of just 'FAILED'. BroadcastJob: logs job id and miner count on every broadcast. Event loop: logs when each new miner socket enters the loop; logs disconnect with session and round share counts. No-job watchdog: RebuildJob retried every 5s (not every 50ms tick), eliminating cs_main hammering when there is no chain tip. AcceptLoopFn: re-polls BtfAllRelays every 60s so relays discovered after startup are picked up; logs each new relay thread spawned. AcceptOneFn: logs relay name on start/stop; logs BtfServiceWrap (echan handshake) failure separately from RvServiceRegister failure. RecvLine: disconnected out-param distinguishes 'socket closed' from 'no complete line yet', preventing incorrect disconnection on partial recv. Participant side (main.cpp): StratumRecvLine: same disconnected out-param fix as RecvLine in rpc.cpp. Previously a false return could mean either 'disconnect' or 'no data yet', causing spurious reconnects when a partial line arrived. PoolParticipantMiner rewritten with step-by-step logging: Step 1 resolve: logs specific failure (no descriptor vs bad format) Step 2 tunnel: logs relay and failure reason Step 3 address: warns explicitly if payout address cannot be determined Step 4 subscribe/authorize: logs each send and each response Step 5 RandomX: logs VM mode Step 6 main loop: - malformed mining.notify (wrong param count or short header/target) logged with sizes instead of silently ignored - authorize rejected: logs reason, does not disconnect (some pools reject bad addresses but still send work) - share accepted/rejected: logs share id and rejection reason - unknown server method logged - pool disconnect logged with handshake state at that moment (gotSubscribeAck, gotAuthorizeAck, haveJob) so you know exactly how far the connection got - nonce space exhausted logged (was silent before) - on exit: logs total hashes, shares sent, shares accepted * Fix build error: ImGuiWindowFlags_NoBringToDisplayFront -> NoBringToFrontOnFocus NoBringToDisplayFront does not exist in this version of ImGui (v1.89.x). The correct flag is ImGuiWindowFlags_NoBringToFrontOnFocus (bit 13). Used in two places: ##statusbar and ##main windows. Also remove dead variable usedY (GetCursorPosY() result assigned but never read) and replace the confusing 'ImGuiWindowFlags_NoScrollbar * 0' expression (evaluates to 0 but looks like a mistake) with a plain 0 in the BeginChild call. * Remove duplicate status logging from SetParticipantMiningStatus Every transition in PoolParticipantMiner already has a direct LogPrint with full context (addresses, job ids, nonces, error reasons). The LogPrint inside SetParticipantMiningStatus was firing on top of those, producing redundant 'PoolParticipantMiner: status -> ...' lines after every meaningful log entry. SetParticipantMiningStatus now just updates the GUI string silently. * Implement vardiff: stable share rate instead of fixed difficulty Previously the pool set shareTarget = blockTarget >> 8 for every miner forever. This meant at low network difficulty a fast miner found shares every few nonces (dozens per second), while at high difficulty a slow miner might go hours without one -- both are bad for fair payout accounting and pool stability. Pool (rpc.cpp): Per-miner difficulty that adjusts to target one share every 15 seconds. After every 5 shares, check actual seconds/share vs target. If the ratio is off by more than 50% (VARDIFF_RETARGET_RATIO), recompute: new_difficulty = current_difficulty / (actual_secs_per_share / target) i.e. shares coming too fast -> difficulty goes up; too slow -> down. Only apply if the change exceeds 10% to avoid jitter. Difficulty is bounded to [0.001, 1e12]. Each miner gets their own shareTarget derived from their difficulty via ShareTargetFromDifficulty(), which maps Stratum difficulty to a uint256 target using the Bitcoin difficulty-1 base (0x1d00ffff compact). New difficulty is sent to the miner via mining.set_difficulty. Participant (main.cpp): Apply the received difficulty value from mining.set_difficulty. Previously the handler was a no-op ('acknowledge only'); now it parses the difficulty double and logs the change. The share target used for comparison comes from the pool's mining.notify p[3] field, which the pool updates after each vardiff adjustment -- this is the standard approach and matches how cpuminer, xmrig etc. handle it. With vardiff, share rate is roughly constant regardless of miner speed or network difficulty. Payouts are proportional to shares submitted, so each miner's payout accurately reflects their CPU contribution. * Fix share target mismatch: pool rejected shares it should have accepted Root cause: the pool put job.shareTarget (from blockTarget<<8, a fixed per-job value) into mining.notify p[3], but checked m->shareTarget (from ShareTargetFromDifficulty, the vardiff value) on submit. With difficulty=1.0, ShareTargetFromDifficulty gives a much HARDER target than blockTarget<<8, so shares that the participant thought were valid (passed the easier notify target) were rejected by the pool (failed the harder difficulty-1 target). Fix: MakeNotifyParams now takes the per-miner shareTarget as an explicit parameter. SendJob passes m->shareTarget. Both sides -- the participant reading p[3] and HandleLine checking m->shareTarget on submit -- now use exactly the same value. Single source of truth. Cleanup: - Remove shareTarget field from StratumJob (was only set, never read after the MakeNotifyParams call was fixed). - Remove MakeShareTarget() function (was only used to populate that now-dead field). - Participant set_difficulty log: 'initial' vs 'updated' instead of 'unchanged' (first connect sends 1.0 which matches the default 1.0, so it was misleadingly logged as 'unchanged'). * Fix double broadcast, correct block reward calculation, improve payout log Double broadcast (event loop vs HandleLine): When a pool miner finds a block, HandleLine calls RebuildJob() + BroadcastJob(true) immediately. The event loop then detects nBestHeight changed (step 2) and was calling RebuildJob() + BroadcastJob(true) again -- miners received duplicate job broadcasts. Fix: step 2 now checks gHaveJob before rebuilding. If HandleLine already built and broadcast a job for this height (gHaveJob=true), step 2 just logs 'job already broadcast by submit handler' and skips. If the block came from an external peer (not our miner), gHaveJob is false and step 2 handles it as before. Block reward calculation in QueuePayouts: Was calling CBlock().GetBlockValue(0) on an empty block object. GetBlockValue uses nBestHeight globally, which by the time QueuePayouts runs has already been incremented to the height AFTER the found block. Fix: compute subsidy directly as '50 COIN >> (blockHeight / 210000)' using the actual found height. At current scale this makes no difference (same subsidy for thousands of blocks) but is now correct by construction. Payout log now shows: - Full block subsidy - Miner share after fee - Operator fee amount in BTF (not just percentage) - Number of recipients, maturity height, dust-filtered count Example: '[payout] block 2096: subsidy 50.00 BTF, miners get 49.00 (fee 2.00% = 1.00 to operator), 3 recipients, matures height 2196' * Fix 6 bugs found in full flow re-read Bug 1 -- Double broadcast (re-applied fix that was lost): When HandleLine accepted a block it called RebuildJob()+BroadcastJob(true). The event loop then detected nBestHeight changed and did it again. Miners received two back-to-back mining.notify clean=true, wasting their work. Fix: step 2 of event loop checks gHaveJob before rebuilding. If HandleLine already built a fresh job (gHaveJob=true), step 2 just logs and skips. Bug 3 -- Disconnect log showed global totals, not session totals: gParticipantSharesSent/Accepted accumulate since node start, not since the last connect. Added sessionSharesSent/sessionSharesAccepted local counters that reset each time PoolParticipantMiner() is called. Bug 4 -- Dead function ParticipantShareTargetFromDiff (main.cpp): Defined but never called anywhere. Removed. Bug 5 -- Invalid-address miner shares silently burned: If a miner authorized with an invalid address, their share of the block reward was neither paid out nor logged -- it just vanished. QueuePayouts now validates addresses up front, logs the forfeited amount explicitly ('X BTF forfeited (bad address)'), and excludes those entries so the payout log is accurate. Bug 6 -- SendMoney called from event loop (stalls miner I/O): FlushMaturePayouts called SendMoney which takes cs_main. If the P2P thread held cs_main (processing a block), the event loop would stall for seconds, causing all connected miners to time out waiting for responses. Fix: FlushMaturePayouts now only moves matured entries to gPayoutExecQueue (fast, no locks except a brief mutex). A new PayoutThreadFn thread drains the queue and calls SendMoney independently, so the event loop is never blocked by wallet I/O. Bug 7 -- Dead goto + dead HandleLine return-false path: HandleLine always returns true; the goto next_miner path was never reached. Removed goto and simplified the select drain loop. * Fix share-target scale mismatch and dead pool hashrate reporting - ShareTargetFromDifficulty() based Stratum difficulty 1 on Bitcoin's SHA256 diff-1 constant (0x1d00ffff, ~2^32 hashes/share) instead of this chain's actual PoW floor (bnProofOfWorkLimit = ~uint256(0)>>12, ~2^12 hashes/share). That's a ~2^20 (~1,048,576x) mismatch: at the supposedly-easy difficulty of 1.0, workers were being asked for shares roughly a million times harder than the chain's own easiest target. Vardiff never gets a chance to correct this because it only retargets after real shares come in, and none were coming in. Fixed by basing the diff-1 reference on bnProofOfWorkLimit instead. - Pool hashrate was never computed anywhere despite the data (per-miner difficulty + share timing) being available: ThreadBtfPoolAnnouncer in nostr.cpp hardcoded ann.hashRate = 0.0, PoolWorkerStatView had no hashrate field, and the GUI workers table had no column for it. Added an EWMA hashrate estimate per miner (updated on each accepted share from expected-hashes-at-difficulty / time-since-last-share), threaded it through GetPoolOperatorStats/GetPoolWorkerStats, the pool announcement payload, and the GUI (new Hashrate column + totals in the telemetry line). Verified: full clean build via src/Makefile (secp256k1 + RandomX deps built from source, g++ 13, all four touched files compile and link). * Remove stale wxWidgets-era build files and orphaned assets These predate the ImGui/GLFW migration (83af065) and are dead weight: - src/makefile.linux, build-linux.sh: superseded by src/Makefile + root Makefile (make linux). The old ones reference obj/uibase.o and obj/ui.o, which don't exist -- they never worked post-migration. - src/makefile.vc: MSVC build for the old wx UI (obj\ui.obj, obj\uibase.obj, obj\irc.obj) -- irc.cpp doesn't even exist anymore, and nothing invokes this makefile. Windows builds go through makefile.mingw via the root Makefile's target. - src/ui.rc, src/uiproject.fbp: wxFormBuilder project/resource files for the old wx UI. The current Windows resource file is bitflash.rc. - src/rc/{addressbook,send}*.bmp, check.ico, bitcoin_bc_original.ico: icons used only by the old wx UI. bitcoin.ico (used by bitflash.rc) is kept. Verified nothing includes uibase.h/ui.h/irc.h anywhere, and did a full clean rebuild afterward (secp256k1 + RandomX from source, src/Makefile) to confirm removal doesn't break the real build. * Mine with RandomX fast (2GB dataset) mode instead of light mode forever RandomXInitDataset() was fully implemented and ready to switch mining into fast mode, but nothing ever called it -- not in PoolParticipantMiner() (pool worker), not in the solo BitcoinMiner() loop. RandomXCreateMinerVM() falls back to light mode whenever the dataset isn't built, so every miner, participant or solo, has always been hashing in light/cache-only mode. Light mode exists for cheap single-hash verification (which is why the pool's own share verification correctly stays on it) -- it recomputes dataset items on the fly for every hash, which is dramatically slower than fast mode's direct dataset lookups. Combined with the share-target scale bug fixed in e87a905, this compounds into exactly the symptom reported: a participant sitting there hashing for many minutes per share instead of the vardiff-targeted ~15s, even after the target fix landed. Fix: build the ~2GB dataset once (multi-threaded, sized to hardware_concurrency) before mining starts, in both the participant and solo paths, so RandomXCreateMinerVM() actually gets to use fast mode. Verified: full clean rebuild via src/Makefile. * Add vardiff stall watchdog: force difficulty down on zero shares, not just share count ShareTargetFromDifficulty already fixed the diff-1 scale mismatch (chain floor vs Bitcoin's SHA256 diff-1), so difficulty 1 correctly means ~4096 expected hashes on this chain, not ~2^32. That part was already correct. But the normal vardiff retarget in HandleLine only runs after VARDIFF_SHARES_PER_CHECK (5) shares arrive. A miner that never finds even one share -- e.g. stuck in RandomX light/cache mode instead of the fast 2GB dataset, or just slower hardware than the target assumes -- never reaches that check, so it can sit at its initial/current difficulty indefinitely with vardiff having no samples to act on. Add a time-based fallback in PoolEventLoop: if a miner's vardiff window has been open for VARDIFF_STALL_SECS (90s, 6x the 15s target) with zero shares, force difficulty down by VARDIFF_STALL_CUT (4x) regardless of share count, and re-notify via mining.set_difficulty. Once a miner is already at VARDIFF_MIN, log it plainly as a hashrate/connectivity issue instead of a difficulty issue, so it's not confused with the same-looking but different problem this was actually fixed for before. * Fix PrintException rethrow killing the whole node on any transient exception The uploaded participant log shows the real bug behind the mining stalls: the node process itself was dying and restarting from scratch every minute or two ('UNKNOWN EXCEPTION ... in ThreadMessageHandler()' / 'ThreadSocketHandler()'), wiping the RandomX dataset/cache and losing all progress toward a share -- not a difficulty problem. Every background thread (ThreadMessageHandler, ThreadSocketHandler, ThreadBtfConnect, the Nostr helpers) wraps its real work in a 'try { Foo2() } catch(...) { log, sleep 5s, loop again }' self-healing retry pattern specifically so one bad exception doesn't take the whole node down. But PrintException(), called from inside every one of those catch blocks, ended with 'if (!fShutdown) throw;' -- re-throwing the just- caught exception during ordinary operation. Nothing above the retry loop catches it, so it escapes the thread and std::terminate()s the entire process on the very first transient error (a flaky relay connect, a malformed message, whatever). The comment's intent (suppress noise while already shutting down) was inverted from what the surrounding retry loops actually need. Removed the rethrow. Threads now log the exception and let their own retry loop handle it, as designed. * Push a fresh job immediately after any difficulty change, not just on next broadcast mining.set_difficulty alone doesn't change what the worker actually hashes against -- the participant only reads its share target from mining.notify's params[3] (see main.cpp's mining.notify handler); the set_difficulty message just updates a locally-logged currentDifficulty value. So both the normal vardiff retarget (in HandleLine, on 5 shares) and the new stall watchdog were updating m->shareTarget and telling the worker about the new difficulty, but the worker kept hashing against its old target until the next natural job broadcast -- a new block, or the 60s trickle refresh. That could delay any difficulty correction by up to a minute for no reason. Call SendJob(m, false) right after each set_difficulty send so the updated shareTarget reaches the worker in the very next tick instead of waiting on the next unrelated broadcast. * Build RandomX dataset before stratum handshake, not after Worker was sending mining.subscribe/authorize, then blocking for the duration of the synchronous ~2GB RandomX dataset build, and only then reading the socket. The pool's replies (subscribe ack, authorize ack, set_difficulty, first job) sat unread in the OS receive buffer for that entire window -- nothing was lost, but with the build taking anywhere from seconds to a couple minutes depending on core count, the worker looked and logged as permanently stuck on 'waiting for pool response' when it just wasn't listening yet. Move the dataset build ahead of the subscribe/authorize sends so the socket is actively being read the moment the pool replies. Also add missing RandomXDestroyMinerVM cleanup on the two early-return paths that now happen after VM creation. * Reset round shares on externally-found blocks, not just our own Round shares were only cleared in HandleLine() after our own miner's block got accepted. When the chain height advanced because someone else found the block, the main loop rebuilt the job but left roundShareCount/roundShareTotal/Miner::roundShares untouched, so they kept accumulating across rounds instead of resetting. * Show pool hashrate in participant view; show immature coinbase value with maturity countdown * Persist pending payouts to disk -- survive operator restarts * Fix log escapes, stall timeout 90->45s, save-before-exec ordering, Immature tx status * Sort transactions by nTimeReceived not chain median time -- new txs always on top * Sort txs by chain depth not nTimeReceived -- survives resync, unconfirmed always first * Fix orphan block false-positive payout; meaningful tx status instead of raw depth * Show Orphaned status in red for orphaned coinbases; distinguish from genuinely unconfirmed --------- Co-authored-by: Vic-Nas Co-authored-by: Bitflash Pool Dev Co-authored-by: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Co-authored-by: Claude --- .gitignore | 1 + Bitflash-1.1.0-windows/glfw3.dll | Bin 0 -> 263756 bytes Bitflash-1.1.0-windows/libcrypto-3-x64.dll | Bin 0 -> 5495100 bytes Bitflash-1.1.0-windows/libdb_cxx-6.2.dll | Bin 0 -> 1666981 bytes Bitflash-1.1.0-windows/libgcc_s_seh-1.dll | Bin 0 -> 153412 bytes Bitflash-1.1.0-windows/libsodium-26.dll | Bin 0 -> 451415 bytes Bitflash-1.1.0-windows/libssl-3-x64.dll | Bin 0 -> 996164 bytes Bitflash-1.1.0-windows/libstdc++-6.dll | Bin 0 -> 2667044 bytes Bitflash-1.1.0-windows/libwinpthread-1.dll | Bin 0 -> 64703 bytes build-linux.sh | 72 - install-bitflash-wine.sh | 211 - src/Makefile | 4 +- src/btfrv.cpp | 4 +- src/db.cpp | 97 +- src/db.h | 15 +- src/gui.cpp | 701 +- src/headers.h | 2 +- src/headers_core.h | 5 +- src/irc.cpp | 271 - src/irc.h | 7 - src/main.cpp | 631 +- src/main.h | 23 +- src/main_gui.cpp | 4 +- src/makefile.linux | 55 - src/makefile.mingw | 4 +- src/makefile.vc | 77 - src/net.cpp | 391 +- src/net.h | 31 +- src/nostr.cpp | 518 +- src/nostr.h | 26 + src/rc/addressbook16.bmp | Bin 1334 -> 0 bytes src/rc/addressbook16mask.bmp | Bin 126 -> 0 bytes src/rc/addressbook20.bmp | Bin 1478 -> 0 bytes src/rc/addressbook20mask.bmp | Bin 142 -> 0 bytes src/rc/bitcoin_bc_original.ico | Bin 22486 -> 0 bytes src/rc/check.ico | Bin 766 -> 0 bytes src/rc/send16.bmp | Bin 1334 -> 0 bytes src/rc/send16mask.bmp | Bin 126 -> 0 bytes src/rc/send16masknoshadow.bmp | Bin 126 -> 0 bytes src/rc/send20.bmp | Bin 1478 -> 0 bytes src/rc/send20mask.bmp | Bin 142 -> 0 bytes src/rpc.cpp | 1544 ++-- src/ui.rc | 14 - src/uiproject.fbp | 11825 --------------------------- src/util.cpp | 108 +- src/util.h | 42 +- 46 files changed, 2648 insertions(+), 14035 deletions(-) create mode 100644 Bitflash-1.1.0-windows/glfw3.dll create mode 100644 Bitflash-1.1.0-windows/libcrypto-3-x64.dll create mode 100644 Bitflash-1.1.0-windows/libdb_cxx-6.2.dll create mode 100644 Bitflash-1.1.0-windows/libgcc_s_seh-1.dll create mode 100644 Bitflash-1.1.0-windows/libsodium-26.dll create mode 100644 Bitflash-1.1.0-windows/libssl-3-x64.dll create mode 100644 Bitflash-1.1.0-windows/libstdc++-6.dll create mode 100644 Bitflash-1.1.0-windows/libwinpthread-1.dll delete mode 100644 build-linux.sh delete mode 100644 install-bitflash-wine.sh delete mode 100644 src/irc.cpp delete mode 100644 src/irc.h delete mode 100644 src/makefile.linux delete mode 100644 src/makefile.vc delete mode 100644 src/rc/addressbook16.bmp delete mode 100644 src/rc/addressbook16mask.bmp delete mode 100644 src/rc/addressbook20.bmp delete mode 100644 src/rc/addressbook20mask.bmp delete mode 100644 src/rc/bitcoin_bc_original.ico delete mode 100644 src/rc/check.ico delete mode 100644 src/rc/send16.bmp delete mode 100644 src/rc/send16mask.bmp delete mode 100644 src/rc/send16masknoshadow.bmp delete mode 100644 src/rc/send20.bmp delete mode 100644 src/rc/send20mask.bmp delete mode 100644 src/ui.rc delete mode 100644 src/uiproject.fbp (limited to 'src/gui.cpp') diff --git a/.gitignore b/.gitignore index 29ebcff..a734faf 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist/ # Editor/OS cruft .DS_Store Thumbs.db +src/bitflash diff --git a/Bitflash-1.1.0-windows/glfw3.dll b/Bitflash-1.1.0-windows/glfw3.dll new file mode 100644 index 0000000..f988a2e Binary files /dev/null and b/Bitflash-1.1.0-windows/glfw3.dll differ diff --git a/Bitflash-1.1.0-windows/libcrypto-3-x64.dll b/Bitflash-1.1.0-windows/libcrypto-3-x64.dll new file mode 100644 index 0000000..76250f0 Binary files /dev/null and b/Bitflash-1.1.0-windows/libcrypto-3-x64.dll differ diff --git a/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll b/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll new file mode 100644 index 0000000..c4279f6 Binary files /dev/null and b/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll differ diff --git a/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll b/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll new file mode 100644 index 0000000..55f4ad1 Binary files /dev/null and b/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll differ diff --git a/Bitflash-1.1.0-windows/libsodium-26.dll b/Bitflash-1.1.0-windows/libsodium-26.dll new file mode 100644 index 0000000..add05ab Binary files /dev/null and b/Bitflash-1.1.0-windows/libsodium-26.dll differ diff --git a/Bitflash-1.1.0-windows/libssl-3-x64.dll b/Bitflash-1.1.0-windows/libssl-3-x64.dll new file mode 100644 index 0000000..cbabbbc Binary files /dev/null and b/Bitflash-1.1.0-windows/libssl-3-x64.dll differ diff --git a/Bitflash-1.1.0-windows/libstdc++-6.dll b/Bitflash-1.1.0-windows/libstdc++-6.dll new file mode 100644 index 0000000..bd3fc0f Binary files /dev/null and b/Bitflash-1.1.0-windows/libstdc++-6.dll differ diff --git a/Bitflash-1.1.0-windows/libwinpthread-1.dll b/Bitflash-1.1.0-windows/libwinpthread-1.dll new file mode 100644 index 0000000..32d8a01 Binary files /dev/null and b/Bitflash-1.1.0-windows/libwinpthread-1.dll differ diff --git a/build-linux.sh b/build-linux.sh deleted file mode 100644 index 4c3844d..0000000 --- a/build-linux.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -# Bitflash headless node/miner -- one-shot Linux build (Debian/Ubuntu). -# Builds the dependencies (libsecp256k1 with schnorrsig, RandomX) and then the -# node itself. Run from the project root (the directory that contains src/): -# -# bash build-linux.sh -# -# Result: src/bitflash-node . Run it with: ./src/bitflash-node -gen - -set -e -ROOT="$(cd "$(dirname "$0")" && pwd)" -cd "$ROOT" - -# Use sudo only when not already root (a root login often has no sudo installed). -SUDO="" -if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi - -echo "==> installing system packages" -if command -v apt-get >/dev/null 2>&1; then - $SUDO apt-get update - $SUDO apt-get install -y build-essential cmake git pkg-config autoconf libtool \ - libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev libboost-dev \ - libboost-system-dev libwxgtk3.2-dev -else - echo "!! non-apt system: install manually -> g++ cmake git autoconf libtool" - echo " libssl-dev libdb++-dev libsodium-dev nlohmann-json (headers)" -fi - -# ---- libsecp256k1 (Schnorr/BIP340 + extrakeys), static, installed to /usr/local -if [ ! -f /usr/local/lib/libsecp256k1.a ]; then - echo "==> building libsecp256k1" - rm -rf "$ROOT/secp256k1-build" - git clone --depth 1 https://github.com/bitcoin-core/secp256k1 "$ROOT/secp256k1-build" - cd "$ROOT/secp256k1-build" - ./autogen.sh - ./configure --enable-module-schnorrsig --enable-module-extrakeys \ - --disable-shared --with-pic --disable-benchmark --disable-tests - make -j"$(nproc)" - $SUDO make install - $SUDO ldconfig - cd "$ROOT" -else - echo "==> libsecp256k1 already installed, skipping" -fi - -# ---- RandomX (memory-hard CPU PoW), static lib -if [ ! -f "$HOME/RandomX/build/librandomx.a" ]; then - echo "==> building RandomX" - rm -rf "$HOME/RandomX" - git clone --depth 1 https://github.com/tevador/RandomX "$HOME/RandomX" - mkdir -p "$HOME/RandomX/build" - cd "$HOME/RandomX/build" - cmake .. -DCMAKE_BUILD_TYPE=Release - make -j"$(nproc)" randomx - cd "$ROOT" -else - echo "==> RandomX already built, skipping" -fi - -# ---- build -echo "==> building bitflash" -cd "$ROOT/src" -make -f makefile.linux RANDOMX_DIR="$HOME/RandomX" -j"$(nproc)" - -echo -echo "===================================================================" -echo " DONE. $ROOT/src/bitflash" -echo -echo " GUI: ./src/bitflash" -echo " Daemon mode: ./src/bitflash /nogui /gen" -echo " Pool server starts automatically on Stratum :3333, stats :19012" -echo "===================================================================" diff --git a/install-bitflash-wine.sh b/install-bitflash-wine.sh deleted file mode 100644 index 91fef59..0000000 --- a/install-bitflash-wine.sh +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env bash -# Bitflash GUI installer for Linux (via Wine) — hardened -set -euo pipefail - -INSTALL_DIR="$HOME/.btf-wine" -WINE_DATA="$HOME/.wine/drive_c/users/$USER/AppData/Roaming/Bitflash" -LINUX_DATA="$HOME/.bitflash" -BIN_DIR="$HOME/.local/bin" -REPO="Bitflash-sh/bitflash" - -echo "==> Bitflash GUI installer" -echo - -# ---- Safety: don't run as root (breaks Wine + pollutes /root) -if [ "$(id -u)" -eq 0 ]; then - echo "ERROR: do not run this as root. Run as your normal user (it will call sudo only when needed)." >&2 - exit 1 -fi - -need() { command -v "$1" >/dev/null 2>&1; } - -# ---- Wine -echo "==> Checking Wine..." -if ! need wine; then - echo " Installing Wine..." - if ! need apt-get; then - echo "ERROR: apt-get not found. Install 'wine' manually, then re-run." >&2 - exit 1 - fi - sudo apt-get update -qq - sudo apt-get install -y wine -fi -echo " Wine: $(wine --version)" - -# ---- db-util (db_dump / db_load for wallet conversion) -echo "==> Checking db-util..." -if ! need db_dump; then - echo " Installing db-util..." - sudo apt-get install -y db-util -fi -echo " db_dump: $(command -v db_dump)" - -# ---- unzip + sha256sum must exist -for tool in unzip sha256sum curl; do - if ! need "$tool"; then - echo " Installing $tool..." - sudo apt-get install -y "$tool" - fi -done - -# ---- Resolve latest release (single API call) -echo "==> Fetching latest release metadata from GitHub..." -API_JSON=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest") - -RELEASE_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE 'win|windows|gui' | head -1 | cut -d '"' -f 4 || true) -[ -z "$RELEASE_URL" ] && RELEASE_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE '\.zip"?$' | head -1 | cut -d '"' -f 4 || true) - -if [ -z "$RELEASE_URL" ]; then - echo "ERROR: no downloadable release asset found at https://github.com/$REPO/releases" >&2 - exit 1 -fi -ASSET_NAME=$(basename "$RELEASE_URL") -echo " Asset: $ASSET_NAME" - -# checksum manifest published alongside the release (SHA256SUMS) -SUMS_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE 'sha256sums|checksums|\.sha256"?$' | head -1 | cut -d '"' -f 4 || true) - -# ---- Download the release -TMPZIP=$(mktemp /tmp/bitflash-XXXXXX.zip) -cleanup() { rm -f "$TMPZIP" "${TMPSUMS:-}"; } -trap cleanup EXIT -echo "==> Downloading release..." -curl -fL --retry 3 --retry-delay 2 -o "$TMPZIP" "$RELEASE_URL" -echo " Downloaded: $(du -h "$TMPZIP" | cut -f1)" - -# ---- MANDATORY integrity check -echo "==> Verifying integrity..." -GOT=$(sha256sum "$TMPZIP" | awk '{print $1}') -if [ -n "${EXPECTED_SHA256:-}" ]; then - # Strongest: user pinned a known-good hash out-of-band. - if [ "$GOT" != "$EXPECTED_SHA256" ]; then - echo "ERROR: checksum mismatch. expected $EXPECTED_SHA256, got $GOT" >&2 - exit 1 - fi - echo " OK (matches pinned EXPECTED_SHA256)" -elif [ -n "$SUMS_URL" ]; then - TMPSUMS=$(mktemp /tmp/bitflash-sums-XXXXXX) - curl -fsSL --retry 3 -o "$TMPSUMS" "$SUMS_URL" - WANT=$(grep -iF "$ASSET_NAME" "$TMPSUMS" | awk '{print $1}' | head -1 || true) - if [ -z "$WANT" ]; then - echo "ERROR: no checksum for $ASSET_NAME in published SHA256SUMS." >&2 - exit 1 - fi - if [ "$GOT" != "$WANT" ]; then - echo "ERROR: checksum mismatch. published $WANT, got $GOT" >&2 - exit 1 - fi - echo " OK (matches published SHA256SUMS)" -else - echo "ERROR: no checksum available for this release." >&2 - echo " Publish a SHA256SUMS asset with the release, or re-run with" >&2 - echo " EXPECTED_SHA256= to verify against a known-good value." >&2 - echo " (To bypass at your own risk: ALLOW_UNVERIFIED=1)" >&2 - if [ "${ALLOW_UNVERIFIED:-0}" != "1" ]; then - exit 1 - fi - echo " WARNING: proceeding UNVERIFIED (ALLOW_UNVERIFIED=1). sha256=$GOT" -fi - -# ---- Extract -echo "==> Extracting to $INSTALL_DIR..." -rm -rf "$INSTALL_DIR" -mkdir -p "$INSTALL_DIR" -unzip -j -o "$TMPZIP" -d "$INSTALL_DIR" >/dev/null - -# ---- Rename backslash-mangled entries (Windows zip paths) -count=0 -for f in "$INSTALL_DIR"/*; do - [ -f "$f" ] || continue - base=$(basename "$f") - newname=$(printf '%s' "$base" | sed 's/.*\\//') - if [ "$newname" != "$base" ]; then - mv "$f" "$INSTALL_DIR/$newname" - count=$((count+1)) - fi -done -echo " Renamed $count files; total: $(ls "$INSTALL_DIR" | wc -l)" - -# Locate the Bitflash exe case-insensitively (zip may ship Bitflash.exe or bitflash.exe) -EXE=$(find "$INSTALL_DIR" -maxdepth 1 -iname 'bitflash.exe' | head -1) -if [ -z "$EXE" ]; then - echo "ERROR: bitflash exe not found after extract. Contents:" >&2 - ls "$INSTALL_DIR" >&2 - exit 1 -fi -echo " Found $(basename "$EXE")" - -# ---- Init Wine data dir -echo "==> Initializing Wine data directory..." -WINEDLLOVERRIDES="libdb_cxx-6.2=n" wine "$EXE" & -WINE_PID=$! -created=0 -for i in $(seq 1 30); do - sleep 1 - if [ -f "$WINE_DATA/wallet.dat" ]; then - echo " wallet.dat created after ${i}s" - created=1 - break - fi -done -kill "$WINE_PID" 2>/dev/null || true -wait "$WINE_PID" 2>/dev/null || true -sleep 1 -mkdir -p "$WINE_DATA" -[ "$created" -eq 1 ] || echo " NOTE: Wine did not create wallet.dat in 30s; continuing." - -# ---- Optional wallet import (with backup) -echo -if [ -f "$LINUX_DATA/wallet.dat" ]; then - echo "==> Found existing Linux wallet at $LINUX_DATA/wallet.dat" - read -r -p " Import it into the GUI wallet? [Y/n] " ans - ans=${ans:-Y} - if [[ "$ans" =~ ^[Yy] ]]; then - if [ -f "$WINE_DATA/wallet.dat" ]; then - BK="$WINE_DATA/wallet.dat.bak.$(date +%Y%m%d%H%M%S)" - cp "$WINE_DATA/wallet.dat" "$BK" - echo " Backed up existing GUI wallet -> $BK" - fi - DUMP=$(mktemp); CONVERTED=$(mktemp); rm -f "$CONVERTED" - echo " Converting Linux wallet format..." - db_dump -p "$LINUX_DATA/wallet.dat" > "$DUMP" - db_load -f "$DUMP" "$CONVERTED" - cp "$CONVERTED" "$WINE_DATA/wallet.dat" - rm -f "$DUMP" "$CONVERTED" - echo " Wallet imported." - else - echo " Skipped. Re-run to import later." - fi -else - echo "==> No Linux wallet found, starting fresh." -fi - -# ---- Launcher -echo "==> Creating 'bitflash' command..." -mkdir -p "$BIN_DIR" -LAUNCHER_FILE="$BIN_DIR/bitflash" -cat > "$LAUNCHER_FILE" </dev/null; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" - echo " Added ~/.local/bin to PATH (restart shell or: source ~/.bashrc)" -fi - -echo -echo "==================================================================" -echo " DONE." -echo " Launch: bitflash" -echo " Mine: bitflash -gen" -echo " Relay: bitflash -announcerelay=YOUR_HOST:8434" -echo " Data: $WINE_DATA" -echo "==================================================================" diff --git a/src/Makefile b/src/Makefile index 3280fd3..c0b6e1d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -21,14 +21,14 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ -lglfw -lGL HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ - key.h bignum.h script.h db.h base58.h net.h irc.h nostr.h \ + key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h compat.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ + obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o GUI_OBJS = obj/gui.o obj/main_gui.o diff --git a/src/btfrv.cpp b/src/btfrv.cpp index 812d077..0d02ca8 100644 --- a/src/btfrv.cpp +++ b/src/btfrv.cpp @@ -101,7 +101,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port) // Apply a short timeout only for the connect() and initial handshake so an // unresponsive relay doesn't block the thread. Once the tunnel is live the - // timeout is cleared — a forwarding pipe must block indefinitely on recv(). + // timeout is cleared -- a forwarding pipe must block indefinitely on recv(). #ifdef _WIN32 DWORD tvShort = 10000; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort)); @@ -118,7 +118,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port) } freeaddrinfo(res); - // Clear the timeout — the socket is now a live tunnel and must not + // Clear the timeout -- the socket is now a live tunnel and must not // time out when idle. recv() will block until data arrives or peer closes. #ifdef _WIN32 DWORD tvOff = 0; diff --git a/src/db.cpp b/src/db.cpp index de71a57..199fa44 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -392,77 +392,8 @@ bool CTxDB::LoadBlockIndex() -// -// CAddrDB -// - -bool CAddrDB::WriteAddress(const CAddress& addr) -{ - return Write(make_pair(string("addr"), addr.GetKey()), addr); -} - -bool CAddrDB::LoadAddresses() -{ - CRITICAL_BLOCK(cs_mapAddresses) - { - // Load user provided addresses - CAutoFile filein = fopen("addr.txt", "rt"); - if (filein) - { - try - { - char psz[1000]; - while (fgets(psz, sizeof(psz), filein)) - { - CAddress addr(psz, NODE_NETWORK); - if (addr.ip != 0) - AddAddress(*this, addr); - } - } - catch (...) { } - } - - // Get cursor - Dbc* pcursor = GetCursor(); - if (!pcursor) - return false; - - loop - { - // Read next record - CDataStream ssKey; - CDataStream ssValue; - int ret = ReadAtCursor(pcursor, ssKey, ssValue); - if (ret == DB_NOTFOUND) - break; - else if (ret != 0) - return false; - - // Unserialize - string strType; - ssKey >> strType; - if (strType == "addr") - { - CAddress addr; - ssValue >> addr; - mapAddresses.insert(make_pair(addr.GetKey(), addr)); - } - } - - //// debug print - printf("mapAddresses:\n"); - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - item.second.print(); - printf("-----\n"); - } - - return true; -} - -bool LoadAddresses() -{ - return CAddrDB("cr+").LoadAddresses(); -} +// Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- +// peer discovery is entirely Nostr/.btf-based now. @@ -563,24 +494,28 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { string strKey; ssKey >> strKey; - if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins; if (strKey == "nTransactionFee") ssValue >> nTransactionFee; if (strKey == "addrIncoming") ssValue >> addrIncoming; - if (strKey == "nMineMode") ssValue >> nMineMode; - if (strKey == "strParticipantPool") ssValue >> strParticipantPool; - if (strKey == "strPoolName") ssValue >> strPoolName; - if (strKey == "strPoolDashboardUrl") ssValue >> strPoolDashboardUrl; - if (strKey == "dPoolFeePercent") ssValue >> dPoolFeePercent; + // Mining-mode/pool settings (nMineMode, strParticipantPool, strPoolName, + // strPoolDashboardUrl, dPoolFeePercent, fGenerateBitcoins) are intentionally + // NOT restored here. They used to be, and LoadWallet() runs after CLI flags + // are parsed in main_gui.cpp, so a saved value would silently overwrite + // whatever was just requested on the command line or in a previous Options + // session -- no error, no log line. That's what caused pool name/address to + // show correctly in the UI but stay stale in the logs, and mode to change + // behavior across restarts. Mode/pool config is decided fresh every launch + // instead: CLI flags if given, otherwise the compiled-in default. Nothing to + // go stale, nothing to fight over load order. } } } - printf("fGenerateBitcoins = %d\n", fGenerateBitcoins); printf("nTransactionFee = %lld\n", nTransactionFee); printf("addrIncoming = %s\n", addrIncoming.ToString().c_str()); - printf("nMineMode = %d, strParticipantPool = %s\n", nMineMode, strParticipantPool.c_str()); - printf("strPoolName = %s, dPoolFeePercent = %.2f\n", strPoolName.c_str(), dPoolFeePercent); - printf("strPoolDashboardUrl = %s\n", strPoolDashboardUrl.c_str()); + printf("nMineMode = %d, strParticipantPool = %s, fGenerateBitcoins = %d (from CLI/default, not wallet.dat)\n", + nMineMode, strParticipantPool.c_str(), fGenerateBitcoins); + printf("strPoolName = %s, dPoolFeePercent = %.2f, strPoolDashboardUrl = %s (from CLI/default, not wallet.dat)\n", + strPoolName.c_str(), dPoolFeePercent, strPoolDashboardUrl.c_str()); return true; } diff --git a/src/db.h b/src/db.h index feaa45e..6dc3a4f 100644 --- a/src/db.h +++ b/src/db.h @@ -316,19 +316,8 @@ private: -class CAddrDB : public CDB -{ -public: - CAddrDB(const char* pszMode="r+", bool fTxn=false) : CDB("addr.dat", pszMode, fTxn) { } -private: - CAddrDB(const CAddrDB&); - void operator=(const CAddrDB&); -public: - bool WriteAddress(const CAddress& addr); - bool LoadAddresses(); -}; - -bool LoadAddresses(); +// Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- +// peer discovery is entirely Nostr/.btf-based now. diff --git a/src/gui.cpp b/src/gui.cpp index 2210296..a1a0698 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1,4 +1,4 @@ -// Bitflash GUI — ImGui + GLFW + OpenGL3 +// Bitflash GUI -- ImGui + GLFW + OpenGL3 // Same source on Linux and Windows. No platform ifdefs. #include "imgui/imgui.h" @@ -25,6 +25,7 @@ extern std::string strParticipantPool; extern CAddress addrLocalHost; void MainFrameRepaint(); +int GetDiscoveredPeerCount(); string DateTimeStr(int64 nTime); bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); int64 GetBalance(); @@ -34,15 +35,20 @@ void ThreadBitcoinMiner(void*); void ThreadRPCServer(void*); // --------------------------------------------------------------------------- -// State +// Layout constants +// --------------------------------------------------------------------------- +static const float STATUS_BAR_H = 22.0f; + +// --------------------------------------------------------------------------- +// GUI state // --------------------------------------------------------------------------- static std::string g_myAddress; -static int64 g_balance = 0; +static int64 g_balance = 0; static int64 g_lastWalletRefresh = 0; -static bool g_showSend = false; -static bool g_showOptions = false; -static bool g_showAbout = false; -static bool g_needRefresh = true; +static bool g_showSend = false; +static bool g_showOptions = false; +static bool g_showAbout = false; +static bool g_needRefresh = true; static char g_sendAddr[128] = {}; static char g_sendAmount[32] = {}; @@ -73,36 +79,34 @@ static std::string FmtMoney(int64 n) static std::string FmtAge(int64 createdAt) { int64 age = GetTime() - createdAt; - if (age < 0) - age = 0; - if (age < 60) - return strprintf("%llds", (long long)age); - if (age < 3600) - return strprintf("%lldm", (long long)(age / 60)); - return strprintf("%lldh", (long long)(age / 3600)); + if (age < 0) age = 0; + if (age < 60) return strprintf("%llds", (long long)age); + if (age < 3600) return strprintf("%lldm", (long long)(age / 60)); + return strprintf("%lldh", (long long)(age / 3600)); } -static ImVec4 ParticipantStatusColor(const std::string& status) +static ImVec4 StatusColor(const std::string& s) { - if (status.find("failed") != std::string::npos || - status.find("rejected") != std::string::npos || - status.find("stopped") != std::string::npos) - return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); - if (status.find("hashing") != std::string::npos || - status.find("accepted") != std::string::npos || - status.find("connected") != std::string::npos) - return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); - if (status.find("asking") != std::string::npos || - status.find("authoriz") != std::string::npos || - status.find("subscrib") != std::string::npos) - return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); - return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); + if (s.find("failed") != std::string::npos || + s.find("rejected") != std::string::npos || + s.find("stopped") != std::string::npos) + return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); // red + if (s.find("hashing") != std::string::npos || + s.find("accepted") != std::string::npos || + s.find("connected")!= std::string::npos) + return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); // green + if (s.find("asking") != std::string::npos || + s.find("authoriz") != std::string::npos || + s.find("subscrib") != std::string::npos) + return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); // amber + return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); // blue-grey } static std::string PoolLabel(const BtfPoolAnnouncement& ann) { std::ostringstream o; - o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; + o << ann.poolName << " - " + << std::fixed << std::setprecision(2) << ann.feePercent << "%"; if (ann.connectedMiners > 0) o << " - " << ann.connectedMiners << " miners"; return o.str(); @@ -118,19 +122,47 @@ static void RefreshWallet() g_txRows.clear(); TRY_CRITICAL_BLOCK(cs_mapWallet) { - std::vector> vs; - for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first}); - std::sort(vs.rbegin(), vs.rend()); + // Sort: unconfirmed first (depth 0), then by depth ascending (least + // deep = most recent). We can't use nTimeReceived because a node + // resync re-stamps all txs with the same time. Depth is always correct. + std::vector> vs; + for (auto& kv : mapWallet) + vs.push_back({kv.second.GetDepthInMainChain(), kv.second.GetHash()}); + std::sort(vs.begin(), vs.end(), + [](const std::pair& a, const std::pair& b) { + return a.first < b.first; // 0 (unconfirmed) first, then shallowest + }); for (auto& sv : vs) { auto mi = mapWallet.find(sv.second); if (mi == mapWallet.end()) continue; CWalletTx& wtx = mi->second; - int64 net = wtx.GetCredit() - wtx.GetDebit(); - int depth = wtx.GetDepthInMainChain(); + int depth = wtx.GetDepthInMainChain(); + int toMature = wtx.IsCoinBase() ? wtx.GetBlocksToMaturity() : 0; + // An orphaned coinbase has hashBlock set but depth=0 (block not in main chain). + // A genuinely unconfirmed tx has hashBlock==0. + bool isOrphaned = wtx.IsCoinBase() && wtx.hashBlock != 0 && depth == 0; + int64 net; std::string desc; - if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated"; - else if (wtx.GetDebit() > 0) desc = "Sent"; - else desc = "Received"; + if (wtx.IsCoinBase()) { + if (isOrphaned) { + net = 0; + desc = "Generated (orphaned -- block not in main chain)"; + } else if (toMature > 0) { + net = wtx.GetValueOut(); + char buf[64]; + snprintf(buf, sizeof(buf), "Generated (matures in %d blocks)", toMature); + desc = buf; + } else { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = depth < 1 ? "Generated (unconfirmed)" : "Generated"; + } + } else if (wtx.GetDebit() > 0) { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = "Sent"; + } else { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = "Received"; + } g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); } } @@ -148,19 +180,104 @@ string DateTimeStr(int64 nTime) return buf; } +// --------------------------------------------------------------------------- +// Status bar -- separate fullscreen-width overlay pinned to bottom edge. +// Drawn last so it always sits on top of everything else. +// --------------------------------------------------------------------------- +static void DrawStatusBar() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y - STATUS_BAR_H)); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, STATUS_BAR_H)); + ImGui::SetNextWindowBgAlpha(1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 3.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.13f, 0.13f, 0.13f, 1.0f)); + ImGui::Begin("##statusbar", nullptr, + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse| + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); + + int connected = (int)vNodes.size(); + int discovered = GetDiscoveredPeerCount(); + + if (nMineMode == MINE_PARTICIPANT) { + uint64 sent = 0, accepted = 0; + double hashRate = 0.0; + std::string st = GetParticipantMiningStatus(); + GetParticipantMiningStats(sent, accepted, hashRate); + if (sent > 0 || accepted > 0 || hashRate > 0.0) + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d" + " Shares: %llu sent / %llu accepted %.2f H/s", + connected, discovered, nBestHeight, + (unsigned long long)sent, (unsigned long long)accepted, hashRate); + else if (!st.empty()) + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d %s", + connected, discovered, nBestHeight, st.c_str()); + else + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d", + connected, discovered, nBestHeight); + } else if (nMineMode == MINE_OPERATOR) { + int miners = 0, blocksFound = 0; + uint64 roundShares = 0; + double totalHashRate = 0.0; + GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d" + " Miners: %d Round shares: %llu Blocks found: %d %.2f H/s", + connected, discovered, nBestHeight, + miners, (unsigned long long)roundShares, blocksFound, totalHashRate); + } else { + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d", + connected, discovered, nBestHeight); + } + + ImGui::End(); +} + // --------------------------------------------------------------------------- // Main window +// +// Layout: +// [menu bar] -- ImGui built-in menu +// [header strip] -- address, balance, send button, mine status +// [separator] +// [telemetry strip] -- mode-specific live stats (never scrolls) +// [separator] +// [scrollable content child] -- transactions + mode panels +// +// The scrollable child ends at the top of the status bar so content never +// hides behind it. The child is explicitly sized to fill the remaining space. // --------------------------------------------------------------------------- static void DrawMainWindow() { ImGuiIO& io = ImGui::GetIO(); - ImGui::SetNextWindowPos(ImVec2(0,0)); - ImGui::SetNextWindowSize(io.DisplaySize); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12)); + + // Full display height minus the status bar at the bottom. + const float winH = io.DisplaySize.y - STATUS_BAR_H; + + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, winH)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f)); ImGui::Begin("##main", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar | - ImGuiWindowFlags_NoFocusOnAppearing); + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_MenuBar | + ImGuiWindowFlags_NoBringToFrontOnFocus); ImGui::SetWindowFontScale(0.92f); ImGui::PopStyleVar(); @@ -170,179 +287,209 @@ static void DrawMainWindow() if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); ImGui::EndMenu(); } - if (ImGui::MenuItem("Options")) g_showOptions = true; - if (ImGui::MenuItem("About")) g_showAbout = true; + if (ImGui::MenuItem("Options")) g_showOptions = true; + if (ImGui::MenuItem("About")) g_showAbout = true; ImGui::EndMenuBar(); } - // ---- Address bar ---- + // ---- Header strip (never scrolls) ---- ImGui::Spacing(); ImGui::TextDisabled("Address:"); ImGui::SameLine(); - ImGui::SetNextItemWidth(-80); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1)); + ImGui::SetNextItemWidth(-80.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f)); ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleColor(); ImGui::SameLine(); if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); - // ---- Balance on its own line ---- ImGui::TextDisabled("Balance:"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f, 1.0f, 0.35f, 1.0f)); ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); ImGui::PopStyleColor(); - // ---- Toolbar ---- ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.40f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); if (ImGui::Button(" Send Coins ")) g_showSend = true; ImGui::PopStyleColor(2); - // Mining status indicator - ImGui::SameLine(0, 20); + ImGui::SameLine(0.0f, 20.0f); if (fGenerateBitcoins) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f)); - const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" : - nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining"; - ImGui::Text("⬤ %s", modeLabel); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); + const char* modeLabel = + nMineMode == MINE_OPERATOR ? "Mining (operator)" : + nMineMode == MINE_PARTICIPANT ? "Mining (pool)" : + nMineMode == MINE_RELAY ? "Relay" : "Mining"; + ImGui::Text("* %s", modeLabel); ImGui::PopStyleColor(); if (nMineMode == MINE_PARTICIPANT) { - std::string mineStatus = GetParticipantMiningStatus(); - if (!mineStatus.empty()) { - ImGui::SameLine(0, 12); - ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus)); - ImGui::Text("[%s]", mineStatus.c_str()); + std::string st = GetParticipantMiningStatus(); + if (!st.empty()) { + ImGui::SameLine(0.0f, 12.0f); + ImGui::PushStyleColor(ImGuiCol_Text, StatusColor(st)); + ImGui::Text("[%s]", st.c_str()); ImGui::PopStyleColor(); } } } else { - ImGui::TextDisabled("○ Not mining"); + ImGui::TextDisabled("Not mining"); } ImGui::Spacing(); ImGui::Separator(); - ImGui::Spacing(); - // ---- Live telemetry strip ---- + // ---- Telemetry strip (never scrolls) ---- { - const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" : - nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo"; - int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1; - if (age < 0) age = 0; + ImGui::Spacing(); + const char* modeName = + nMineMode == MINE_OPERATOR ? "Operator" : + nMineMode == MINE_PARTICIPANT ? "Participant" : + nMineMode == MINE_RELAY ? "Relay" : "Solo"; + int64 age = g_lastWalletRefresh > 0 ? GetTime() - g_lastWalletRefresh : 0; ImGui::Text("Mode: %s", modeName); - ImGui::SameLine(0, 16); - ImGui::Text("Wallet refresh: %llds ago", (long long)age); + ImGui::SameLine(0.0f, 16.0f); + ImGui::TextDisabled("Wallet refreshed %llds ago", (long long)age); if (nMineMode == MINE_PARTICIPANT) { uint64 sent = 0, accepted = 0; double hashRate = 0.0; - std::string mineStatus = GetParticipantMiningStatus(); + std::string st = GetParticipantMiningStatus(); GetParticipantMiningStats(sent, accepted, hashRate); if (sent > 0 || accepted > 0 || hashRate > 0.0) { - double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; - ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s", - (unsigned long long)sent, - (unsigned long long)accepted, - acceptPct, - hashRate); - } else if (!mineStatus.empty()) { - ImGui::TextDisabled("Your mining: %s", mineStatus.c_str()); + double pct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; + ImGui::TextDisabled( + "Sent %llu shares, accepted %llu (%.1f%%), %.2f H/s", + (unsigned long long)sent, (unsigned long long)accepted, pct, hashRate); + } else if (!st.empty()) { + ImGui::TextDisabled("Status: %s", st.c_str()); } else { - ImGui::TextDisabled("Your mining: asking pool for work"); + ImGui::TextDisabled("Waiting for pool work..."); } } else if (nMineMode == MINE_OPERATOR) { int miners = 0, blocksFound = 0; uint64 roundShares = 0; - GetPoolOperatorStats(miners, blocksFound, roundShares); + double totalHashRate = 0.0; + GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); if (miners > 0 || blocksFound > 0 || roundShares > 0) - ImGui::TextDisabled("Pool runtime: %d authorized miners, %llu round shares, %d blocks found", miners, (unsigned long long)roundShares, blocksFound); + ImGui::TextDisabled( + "Authorized miners: %d Round shares: %llu Blocks found: %d %.2f H/s", + miners, (unsigned long long)roundShares, blocksFound, totalHashRate); else - ImGui::TextDisabled("Pool runtime: waiting for miners"); + ImGui::TextDisabled("Pool server running, waiting for miners"); + } else if (nMineMode == MINE_RELAY) { + ImGui::TextDisabled("Relay mode: syncing, not mining"); } else { - ImGui::TextDisabled("Solo runtime: local wallet mining telemetry"); + ImGui::TextDisabled("Solo mining"); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); } + // ---- Scrollable content child ---- + // Fills all remaining height in the window (cursor Y to bottom of window). + // Content that overflows scrolls; nothing leaks past the window edge. + // Size the scrollable child to fill exactly the remaining window height. + // GetContentRegionAvail() already accounts for padding/borders. + const float availH = ImGui::GetContentRegionAvail().y; + ImGui::BeginChild("##scroll", ImVec2(0.0f, availH), false, 0); + // ---- Transaction list ---- - float listHeight = io.DisplaySize.y * 0.26f; - if (listHeight < 190.0f) listHeight = 190.0f; - if (listHeight > 260.0f) listHeight = 260.0f; + float txH = io.DisplaySize.y * 0.26f; + if (txH < 180.0f) txH = 180.0f; + if (txH > 260.0f) txH = 260.0f; + if (ImGui::BeginTable("txlist", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, - ImVec2(0, listHeight))) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_SizingStretchProp, + ImVec2(0.0f, txH))) { ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110); - ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110.0f); + ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130.0f); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140); + ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140.0f); ImGui::TableHeadersRow(); for (auto& row : g_txRows) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - if (row.depth < 1) - ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed"); + bool isImmatureCoinbase = (row.desc.find("matures in") != std::string::npos); + bool isOrphaned = (row.desc.find("orphaned") != std::string::npos); + if (isOrphaned) + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Orphaned"); + else if (row.depth < 1) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Unconfirmed"); + else if (isImmatureCoinbase) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Immature"); + else if (row.depth < 6) + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%d confirm%s", + row.depth, row.depth == 1 ? "" : "s"); else - ImGui::Text("%d blocks deep", row.depth); + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Confirmed"); ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.date.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(row.desc.c_str()); ImGui::TableSetColumnIndex(3); if (row.amount >= 0) - ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str()); + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), + "+%s BTF", FmtMoney(row.amount).c_str()); else - ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "%s BTF", FmtMoney(row.amount).c_str()); } ImGui::EndTable(); } + // ---- Mode panels ---- std::vector pools; BtfGetPoolAnnouncements(pools); if (nMineMode == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Participant Mining"); - ImGui::Text("Selected pool:"); + + ImGui::TextDisabled("Selected pool:"); + ImGui::SameLine(); if (strParticipantPool.empty()) - ImGui::TextDisabled("No pool selected yet"); + ImGui::TextDisabled("none"); else ImGui::TextWrapped("%s", strParticipantPool.c_str()); - const BtfPoolAnnouncement* selected = NULL; + const BtfPoolAnnouncement* sel = nullptr; for (size_t i = 0; i < pools.size(); i++) - if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; } - - if (selected) { - ImGui::Text("Pool name: %s", selected->poolName.c_str()); - ImGui::Text("Fee: %.2f%%", selected->feePercent); - ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound); - if (!selected->dashboardUrl.empty()) - ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str()); + if (pools[i].btfAddress == strParticipantPool) { sel = &pools[i]; break; } + + if (sel) { + ImGui::Text("Pool: %s Fee: %.2f%% Miners: %d Blocks: %d %.2f H/s", + sel->poolName.c_str(), sel->feePercent, + sel->connectedMiners, sel->blocksFound, sel->hashRate); + if (!sel->dashboardUrl.empty()) + ImGui::TextDisabled("Dashboard: %s", sel->dashboardUrl.c_str()); } else if (!strParticipantPool.empty()) { - ImGui::TextDisabled("No fresh announcement found for this pool yet"); + ImGui::TextDisabled("No live announcement for this pool yet."); } if (!pools.empty()) { ImGui::Spacing(); ImGui::SeparatorText("Live Pools"); if (ImGui::BeginTable("livepools", 5, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160); + ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 150.0f); ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableHeadersRow(); for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; @@ -353,15 +500,19 @@ static void DrawMainWindow() ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f%%", ann.feePercent); ImGui::TableSetColumnIndex(2); - ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str()); + ImGui::Text("%d miners %d blocks %.2f H/s %s", + ann.connectedMiners, ann.blocksFound, ann.hashRate, + FmtAge(ann.createdAt).c_str()); ImGui::TableSetColumnIndex(3); if (!ann.dashboardUrl.empty()) ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); else ImGui::TextDisabled("-"); ImGui::TableSetColumnIndex(4); - if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) { - strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); + std::string btnId = "Use##" + ann.btfAddress; + if (ImGui::SmallButton(btnId.c_str())) { + strncpy(g_participantPool, ann.btfAddress.c_str(), + sizeof(g_participantPool)-1); strParticipantPool = ann.btfAddress; CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); g_needRefresh = true; @@ -375,55 +526,60 @@ static void DrawMainWindow() if (nMineMode == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Operator Overview"); - ImGui::Text("Pool .btf address:"); + std::string opAddr = BtfLocalAddress(); + ImGui::TextDisabled("Pool .btf address:"); + ImGui::SameLine(); if (opAddr.empty()) - ImGui::TextDisabled("Identity not ready yet"); + ImGui::TextDisabled("identity not ready"); else ImGui::TextWrapped("%s", opAddr.c_str()); + // Pending payouts std::vector owed; GetPendingPayouts(owed); ImGui::Spacing(); - ImGui::SeparatorText("Pending Pool Payouts (you owe miners)"); + ImGui::SeparatorText("Pending Payouts (owed to miners)"); if (owed.empty()) { - ImGui::TextDisabled("No pending pool payouts"); + ImGui::TextDisabled("No pending payouts"); } else if (ImGui::BeginTable("owedpayouts", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100); - ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Blocks left", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); for (const PendingPayoutView& row : owed) { - int remaining = std::max(0, row.matureAtHeight - nBestHeight); + int left = std::max(0, row.matureAtHeight - nBestHeight); ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%d", row.matureAtHeight); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%d", remaining); - ImGui::TableSetColumnIndex(2); - ImGui::Text("%d", row.recipients); - ImGui::TableSetColumnIndex(3); - ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); + ImGui::TableSetColumnIndex(0); ImGui::Text("%d", row.matureAtHeight); + ImGui::TableSetColumnIndex(1); ImGui::Text("%d", left); + ImGui::TableSetColumnIndex(2); ImGui::Text("%d", row.recipients); + ImGui::TableSetColumnIndex(3); ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); } ImGui::EndTable(); } + // Workers std::vector workers; GetPoolWorkerStats(workers); ImGui::Spacing(); ImGui::SeparatorText("Current Round Workers"); if (workers.empty()) { - ImGui::TextDisabled("No active workers in this round"); - } else if (ImGui::BeginTable("workers", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGui::TextDisabled("No active workers this round"); + } else if (ImGui::BeginTable("workers", 5, + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Payout Address", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Hashrate", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Round shares", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Last seen", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableHeadersRow(); for (const PoolWorkerStatView& row : workers) { ImGui::TableNextRow(); @@ -432,53 +588,19 @@ static void DrawMainWindow() ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); ImGui::TableSetColumnIndex(2); - ImGui::Text("%llu", (unsigned long long)row.roundShares); + ImGui::Text("%.2f H/s", row.hashRate); ImGui::TableSetColumnIndex(3); - ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str()); + ImGui::Text("%llu", (unsigned long long)row.roundShares); + ImGui::TableSetColumnIndex(4); + ImGui::TextDisabled("%s ago", FmtAge(row.lastSeen).c_str()); } ImGui::EndTable(); } } - // ---- Status bar ---- - { - ImGui::Spacing(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,2)); - - int peers = (int)vNodes.size(); - if (nMineMode == MINE_PARTICIPANT) { - uint64 sent = 0, accepted = 0; - double hashRate = 0.0; - std::string mineStatus = GetParticipantMiningStatus(); - GetParticipantMiningStats(sent, accepted, hashRate); - if (sent > 0 || accepted > 0 || hashRate > 0.0) - ImGui::TextDisabled("Peers %d height %d shares sent %llu accepted %llu hash rate %.2f H/s", - peers, - nBestHeight, - (unsigned long long)sent, - (unsigned long long)accepted, - hashRate); - else if (!mineStatus.empty()) - ImGui::TextDisabled("Peers %d height %d %s", peers, nBestHeight, mineStatus.c_str()); - else - ImGui::TextDisabled("Peers %d height %d asking pool for work", peers, nBestHeight); - } else if (nMineMode == MINE_OPERATOR) { - int miners = 0, blocksFound = 0; - uint64 roundShares = 0; - GetPoolOperatorStats(miners, blocksFound, roundShares); - if (miners > 0 || blocksFound > 0 || roundShares > 0) - ImGui::TextDisabled("Peers %d height %d authorized miners %d round shares %llu blocks found %d", - peers, nBestHeight, miners, (unsigned long long)roundShares, blocksFound); - else - ImGui::TextDisabled("Peers %d height %d waiting for miners", peers, nBestHeight); - } else { - ImGui::TextDisabled("Peers %d height %d", peers, nBestHeight); - } - ImGui::PopStyleVar(); - } - + ImGui::EndChild(); // ##scroll ImGui::SetWindowFontScale(1.0f); - ImGui::End(); + ImGui::End(); // ##main } // --------------------------------------------------------------------------- @@ -487,42 +609,53 @@ static void DrawMainWindow() static void DrawSendDialog() { if (!g_showSend) return; - ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + ImGui::SetNextWindowSize(ImVec2(500.0f, 180.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Send Coins", &g_showSend, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::Text("Recipient address:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); ImGui::Text("Amount (BTF):"); - ImGui::SetNextItemWidth(180); + ImGui::SetNextItemWidth(180.0f); ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); if (!g_sendStatus.empty()) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "%s", g_sendStatus.c_str()); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("Send", ImVec2(90,0))) { + if (ImGui::Button("Send", ImVec2(90.0f, 0.0f))) { uint160 h; int64 nv = 0; - if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; - else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount."; + if (!AddressToHash160(g_sendAddr, h)) + g_sendStatus = "Invalid address."; + else if (!ParseMoney(g_sendAmount, nv) || nv <= 0) + g_sendStatus = "Invalid amount."; else { - CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; + CScript sc; + sc << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; CWalletTx wtx; - if (SendMoney(s, nv, wtx)) { - g_showSend=false; g_sendStatus=""; - memset(g_sendAddr,0,sizeof(g_sendAddr)); - memset(g_sendAmount,0,sizeof(g_sendAmount)); + if (SendMoney(sc, nv, wtx)) { + g_showSend = false; + g_sendStatus = ""; + memset(g_sendAddr, 0, sizeof(g_sendAddr)); + memset(g_sendAmount, 0, sizeof(g_sendAmount)); g_needRefresh = true; - } else g_sendStatus = "Transaction failed."; + } else { + g_sendStatus = "Transaction failed."; + } } } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; } + if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) { + g_showSend = false; + g_sendStatus = ""; + } } ImGui::End(); } @@ -534,7 +667,6 @@ static void DrawOptionsDialog() { if (!g_showOptions) return; - // Always sync from current globals when dialog opens static bool wasOpen = false; if (!wasOpen) { g_mineRadio = nMineMode; @@ -544,42 +676,46 @@ static void DrawOptionsDialog() strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); } wasOpen = true; - ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + + ImGui::SetNextWindowSize(ImVec2(500.0f, 335.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Options", &g_showOptions, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { // Transaction fee ImGui::SeparatorText("Transaction Fee"); static char feeStr[32] = {}; - if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str()); - ImGui::SetNextItemWidth(180); + if (feeStr[0] == '\0') + snprintf(feeStr, sizeof(feeStr), "%s", FmtMoney(nTransactionFee).c_str()); + ImGui::SetNextItemWidth(180.0f); ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); - // Mining + // Mining mode ImGui::SeparatorText("Mining Mode"); - ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO); - ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR); - ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); + ImGui::RadioButton("Relay -- just run the node, don't mine", &g_mineRadio, MINE_RELAY); + ImGui::RadioButton("Solo -- rewards go to your wallet", &g_mineRadio, MINE_SOLO); + ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); + ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); if (g_mineRadio == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Pool Announcement"); ImGui::Text("Pool name:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); ImGui::Text("Fee %%:"); - ImGui::SetNextItemWidth(120); + ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); ImGui::Text("Dashboard URL:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); } if (g_mineRadio == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Pool Selection"); - ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here."); + ImGui::TextDisabled("Pick from live discovered pools."); std::vector pools; BtfGetPoolAnnouncements(pools); @@ -591,46 +727,49 @@ static void DrawOptionsDialog() break; } } - if (ImGui::BeginCombo("Discovered pools", current.c_str())) { + if (ImGui::BeginCombo("Discovered pools##combo", current.c_str())) { for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; - std::string label = PoolLabel(ann); - bool selected = ann.btfAddress == g_participantPool; - if (ImGui::Selectable(label.c_str(), selected)) { - strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); - } + bool selected = (ann.btfAddress == g_participantPool); + if (ImGui::Selectable(PoolLabel(ann).c_str(), selected)) + strncpy(g_participantPool, ann.btfAddress.c_str(), + sizeof(g_participantPool)-1); } ImGui::EndCombo(); } } else { - ImGui::TextDisabled("No live pools discovered yet"); + ImGui::TextDisabled("No live pools discovered yet."); } } - ImGui::Spacing(); - const char* genLabel = fGenerateBitcoins ? "Stop Mining" : "Start Mining"; - if (ImGui::Button(genLabel, ImVec2(130,0))) { - fGenerateBitcoins = fGenerateBitcoins ? 0 : 1; - if (fGenerateBitcoins) _beginthread(ThreadBitcoinMiner, 0, NULL); - CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); - } - ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("OK", ImVec2(90,0))) { - int64 fee=0; - if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); } - - bool changed = (g_mineRadio != nMineMode); - nMineMode = g_mineRadio; - strParticipantPool = g_participantPool; - strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; - strPoolDashboardUrl = g_poolDash; - dPoolFeePercent = atof(g_poolFee); - CWalletDB().WriteSetting("nMineMode", nMineMode); - CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); - CWalletDB().WriteSetting("strPoolName", strPoolName); - CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl); - CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent); + if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) { + int64 fee = 0; + if (ParseMoney(feeStr, fee)) { + nTransactionFee = fee; + CWalletDB().WriteSetting("nTransactionFee", nTransactionFee); + } + + bool poolChanged = + nMineMode != g_mineRadio || + strPoolName != (g_poolName[0] ? g_poolName : "Bitflash Pool") || + strPoolDashboardUrl != g_poolDash || + dPoolFeePercent != atof(g_poolFee); + + nMineMode = g_mineRadio; + strParticipantPool = g_participantPool; + strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; + strPoolDashboardUrl = g_poolDash; + dPoolFeePercent = atof(g_poolFee); + + if (nMineMode == MINE_OPERATOR && poolChanged) gAnnounceNow = true; + + if (nMineMode == MINE_RELAY) { + fGenerateBitcoins = 0; + } else { + fGenerateBitcoins = 1; + if (!vfThreadRunning[3]) _beginthread(ThreadBitcoinMiner, 0, NULL); + } if (nMineMode == MINE_OPERATOR) { if (!gPoolServerRunning) { @@ -645,7 +784,7 @@ static void DrawOptionsDialog() g_showOptions = false; } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false; + if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) g_showOptions = false; } if (!g_showOptions) wasOpen = false; ImGui::End(); @@ -657,18 +796,21 @@ static void DrawOptionsDialog() static void DrawAboutDialog() { if (!g_showAbout) return; - ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("About Bitflash", &g_showAbout, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { - ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0"); + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); ImGui::Spacing(); - ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine."); + ImGui::TextWrapped( + "CPU-only cryptocurrency. RandomX proof of work. " + "Anonymous .btf addressing over Nostr. No premine."); ImGui::Spacing(); ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); ImGui::Spacing(); - if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false; + if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) g_showAbout = false; } ImGui::End(); } @@ -705,34 +847,33 @@ int RunGUI(int argc, char* argv[]) ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; - // Load Roboto at 16px — clean, readable on all platforms io.Fonts->AddFontFromMemoryCompressedTTF( - RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f); + RobotoMedium_compressed_data, RobotoMedium_compressed_size, 14.0f); ImGui::StyleColorsDark(); - ImGuiStyle& s = ImGui::GetStyle(); - s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f; - s.ItemSpacing = ImVec2(10, 7); - s.FramePadding = ImVec2(8, 5); - s.WindowPadding = ImVec2(12, 12); + ImGuiStyle& st = ImGui::GetStyle(); + st.WindowRounding = st.FrameRounding = st.PopupRounding = 4.0f; + st.ItemSpacing = ImVec2(10.0f, 7.0f); + st.FramePadding = ImVec2(8.0f, 5.0f); + st.WindowPadding = ImVec2(12.0f, 12.0f); // Orange accent - s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1); - s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1); - s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1); - s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f); - s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f); - s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1); - s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1); - s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1); - s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1); - s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1); - s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1); + st.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, .60f, .00f, 1.0f); + st.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_Button] = ImVec4(.25f, .25f, .25f, 1.0f); + st.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f, .30f, .00f, 1.0f); + st.Colors[ImGuiCol_Header] = ImVec4(.80f, .45f, .00f, .40f); + st.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f, .45f, .20f, .80f); + st.Colors[ImGuiCol_Tab] = ImVec4(.20f, .20f, .20f, 1.0f); + st.Colors[ImGuiCol_TabHovered] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_TabActive] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_SeparatorHovered]= ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_FrameBg] = ImVec4(.14f, .14f, .14f, 1.0f); + st.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f, .20f, .20f, 1.0f); + st.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f, .18f, .18f, 1.0f); + st.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f, .11f, .11f, 1.0f); + st.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f, .14f, .14f, 1.0f); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); @@ -744,22 +885,28 @@ int RunGUI(int argc, char* argv[]) while (!glfwWindowShouldClose(window) && !fShutdown) { glfwPollEvents(); - if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } + if (g_needRefresh || (frame % 120 == 0)) { + RefreshWallet(); + g_needRefresh = false; + } frame++; ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + // Draw order matters: status bar last so it is always on top. DrawMainWindow(); DrawSendDialog(); DrawOptionsDialog(); DrawAboutDialog(); + DrawStatusBar(); ImGui::Render(); - int w, h; glfwGetFramebufferSize(window, &w, &h); + int w, h; + glfwGetFramebufferSize(window, &w, &h); glViewport(0, 0, w, h); - glClearColor(.08f,.08f,.08f,1); + glClearColor(.08f, .08f, .08f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); diff --git a/src/headers.h b/src/headers.h index bb252c7..367e9b0 100644 --- a/src/headers.h +++ b/src/headers.h @@ -1,4 +1,4 @@ -// Bitflash — main include file. +// Bitflash -- main include file. // Includes core node headers. GUI is handled separately in gui.cpp. #pragma once #include "headers_core.h" diff --git a/src/headers_core.h b/src/headers_core.h index 089b67f..6c0c602 100644 --- a/src/headers_core.h +++ b/src/headers_core.h @@ -1,4 +1,4 @@ -// Core node headers — no GUI framework dependency. +// Core node headers -- no GUI framework dependency. // Included by gui.cpp and all non-GUI source files. #ifdef _MSC_VER @@ -63,12 +63,11 @@ inline typename std::common_type::type max(const T1& a, const T2& b) #include "script.h" #include "db.h" #include "net.h" -#include "irc.h" #include "nostr.h" #include "randomx_pow.h" #include "main.h" #include "market.h" -// GUI callback — implemented in gui.cpp (or as no-op in headless builds) +// GUI callback -- implemented in gui.cpp (or as no-op in headless builds) void MainFrameRepaint(); string DateTimeStr(int64 nTime); diff --git a/src/irc.cpp b/src/irc.cpp deleted file mode 100644 index ff82c66..0000000 --- a/src/irc.cpp +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -#include "headers.h" - - - - -#pragma pack(1) -struct ircaddr -{ - int ip; - short port; -}; - -string EncodeAddress(const CAddress& addr) -{ - struct ircaddr tmp; - tmp.ip = addr.ip; - tmp.port = addr.port; - - vector vch(UBEGIN(tmp), UEND(tmp)); - return string("u") + EncodeBase58Check(vch); -} - -bool DecodeAddress(string str, CAddress& addr) -{ - vector vch; - if (!DecodeBase58Check(str.substr(1), vch)) - return false; - - struct ircaddr tmp; - if (vch.size() != sizeof(tmp)) - return false; - memcpy(&tmp, &vch[0], sizeof(tmp)); - - addr = CAddress(tmp.ip, tmp.port); - return true; -} - - - - - - -static bool Send(SOCKET hSocket, const char* pszSend) -{ - if (strstr(pszSend, "PONG") != pszSend) - printf("SENDING: %s\n", pszSend); - const char* psz = pszSend; - const char* pszEnd = psz + strlen(psz); - while (psz < pszEnd) - { - int ret = send(hSocket, psz, pszEnd - psz, 0); - if (ret < 0) - return false; - psz += ret; - } - return true; -} - -bool RecvLine(SOCKET hSocket, string& strLine) -{ - strLine = ""; - loop - { - char c; - int nBytes = recv(hSocket, &c, 1, 0); - if (nBytes > 0) - { - if (c == '\n') - continue; - if (c == '\r') - return true; - strLine += c; - } - else if (nBytes <= 0) - { - if (!strLine.empty()) - return true; - // socket closed - printf("IRC socket closed\n"); - return false; - } - else - { - // socket error - int nErr = WSAGetLastError(); - if (nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) - { - printf("IRC recv failed: %d\n", nErr); - return false; - } - } - } -} - -bool RecvLineIRC(SOCKET hSocket, string& strLine) -{ - loop - { - bool fRet = RecvLine(hSocket, strLine); - if (fRet) - { - if (fShutdown) - return false; - vector vWords; - ParseString(strLine, ' ', vWords); - if (vWords[0] == "PING") - { - strLine[1] = 'O'; - strLine += '\r'; - Send(hSocket, strLine.c_str()); - continue; - } - } - return fRet; - } -} - -bool RecvUntil(SOCKET hSocket, const char* psz1, const char* psz2=NULL, const char* psz3=NULL) -{ - loop - { - string strLine; - if (!RecvLineIRC(hSocket, strLine)) - return false; - printf("IRC %s\n", strLine.c_str()); - if (psz1 && strLine.find(psz1) != -1) - return true; - if (psz2 && strLine.find(psz2) != -1) - return true; - if (psz3 && strLine.find(psz3) != -1) - return true; - } -} - - - - -bool fRestartIRCSeed = false; - -void ThreadIRCSeed(void* parg) -{ - // IRC peer discovery disabled: the 2009 IRC network no longer exists (and - // what answers today crashes the parser). Replaced by Nostr relay - // discovery. - printf("ThreadIRCSeed: IRC peer discovery disabled (using Nostr)\n"); - return; - - loop - { - struct hostent* phostent = gethostbyname("chat.freenode.net"); - CAddress addrConnect(*(u_long*)phostent->h_addr_list[0], htons(6667)); - - SOCKET hSocket; - if (!ConnectSocket(addrConnect, hSocket)) - { - printf("IRC connect failed\n"); - return; - } - - if (!RecvUntil(hSocket, "Found your hostname", "using your IP address instead", "Couldn't look up your hostname")) - { - closesocket(hSocket); - return; - } - - string strMyName = EncodeAddress(addrLocalHost); - - if (!addrLocalHost.IsRoutable()) - strMyName = strprintf("x%u", GetRand(1000000000)); - - Send(hSocket, strprintf("NICK %s\r", strMyName.c_str()).c_str()); - Send(hSocket, strprintf("USER %s 8 * : %s\r", strMyName.c_str(), strMyName.c_str()).c_str()); - - if (!RecvUntil(hSocket, " 004 ")) - { - closesocket(hSocket); - return; - } - Sleep(500); - - Send(hSocket, "JOIN #bitcoin\r"); - Send(hSocket, "WHO #bitcoin\r"); - - while (!fRestartIRCSeed) - { - string strLine; - if (fShutdown || !RecvLineIRC(hSocket, strLine)) - { - closesocket(hSocket); - return; - } - if (strLine.empty() || strLine[0] != ':') - continue; - printf("IRC %s\n", strLine.c_str()); - - vector vWords; - ParseString(strLine, ' ', vWords); - if (vWords.size() < 2) - continue; - - char pszName[10000]; - pszName[0] = '\0'; - - if (vWords[1] == "352" && vWords.size() >= 8) - { - // index 7 is limited to 16 characters - // could get full length name at index 10, but would be different from join messages - strcpy(pszName, vWords[7].c_str()); - printf("GOT WHO: [%s] ", pszName); - } - - if (vWords[1] == "JOIN") - { - // :username!username@50000007.F000000B.90000002.IP JOIN :#channelname - strcpy(pszName, vWords[0].c_str() + 1); - if (strchr(pszName, '!')) - *strchr(pszName, '!') = '\0'; - printf("GOT JOIN: [%s] ", pszName); - } - - if (pszName[0] == 'u') - { - CAddress addr; - if (DecodeAddress(pszName, addr)) - { - CAddrDB addrdb; - if (AddAddress(addrdb, addr)) - printf("new "); - addr.print(); - } - else - { - printf("decode failed\n"); - } - } - } - - fRestartIRCSeed = false; - closesocket(hSocket); - } -} - - - - - - - - - - -#ifdef TEST -int main(int argc, char *argv[]) -{ - WSADATA wsadata; - if (WSAStartup(MAKEWORD(2,2), &wsadata) != NO_ERROR) - { - printf("Error at WSAStartup()\n"); - return false; - } - - ThreadIRCSeed(NULL); - - WSACleanup(); - return 0; -} -#endif diff --git a/src/irc.h b/src/irc.h deleted file mode 100644 index 1ae4ffc..0000000 --- a/src/irc.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -extern bool RecvLine(SOCKET hSocket, string& strLine); -extern void ThreadIRCSeed(void* parg); -extern bool fRestartIRCSeed; diff --git a/src/main.cpp b/src/main.cpp index f3de4d3..aff7bab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "sha.h" #include #include +#include #include "btfaddr.h" #include "btftunnel.h" #ifndef _WIN32 @@ -73,7 +74,7 @@ bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local t int fGenerateBitcoins; int64 nTransactionFee = 0; CAddress addrIncoming; -int nMineMode = MINE_SOLO; +int nMineMode = MINE_RELAY; string strParticipantPool; // participant mode: pool .btf address string strPoolName = "Bitflash Pool"; string strPoolDashboardUrl; @@ -88,11 +89,10 @@ static std::string gParticipantStatus; void SetParticipantMiningStatus(const std::string& status) { + // Updates the GUI status string only. Logging is done at each call site + // with full context; duplicating it here just adds noise to the log. std::lock_guard lk(gParticipantStatusMutex); - if (gParticipantStatus == status) - return; gParticipantStatus = status; - printf("PoolParticipantMiner: status -> %s\n", status.c_str()); } std::string GetParticipantMiningStatus() @@ -159,7 +159,7 @@ bool AddToWallet(const CWalletTx& wtxIn) wtx.nTimeReceived = GetAdjustedTime(); //// debug print - printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update"); + if (LogAcceptsCategory("net")) printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update"); if (!fInsertedNew) { @@ -372,7 +372,7 @@ int CMerkleTx::SetMerkleBranch(const CBlock* pblock) { vMerkleBranch.clear(); nIndex = -1; - printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n"); + if (LogAcceptsCategory("net")) printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n"); return 0; } @@ -433,7 +433,7 @@ void CWalletTx::AddSupportingTransactions(CTxDB& txdb) } else { - printf("ERROR: AddSupportingTransactions() : unsupported transaction\n"); + if (LogAcceptsCategory("net")) printf("ERROR: AddSupportingTransactions() : unsupported transaction\n"); continue; } @@ -519,7 +519,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis { if (ptxOld) { - printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str()); + if (LogAcceptsCategory("net")) printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str()); mapTransactions.erase(ptxOld->GetHash()); } AddToMemoryPool(); @@ -530,7 +530,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis if (ptxOld) EraseFromWallet(ptxOld->GetHash()); - printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str()); return true; } @@ -669,7 +669,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb) uint256 hash = GetHash(); if (!txdb.ContainsTx(hash)) { - printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); } } @@ -683,7 +683,7 @@ void RelayWalletTransactions() nLastTime = GetTime(); // Rebroadcast any of our txes that aren't in a block yet - printf("RelayWalletTransactions()\n"); + if (LogAcceptsCategory("net")) printf("RelayWalletTransactions()\n"); CTxDB txdb("r"); CRITICAL_BLOCK(cs_mapWallet) { @@ -764,7 +764,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast) // Limit adjustment step unsigned int nActualTimespan = pindexLast->nTime - pindexFirst->nTime; - printf(" nActualTimespan = %d before bounds\n", nActualTimespan); + if (LogAcceptsCategory("net")) printf(" nActualTimespan = %d before bounds\n", nActualTimespan); if (nActualTimespan < nTargetTimespan/4) nActualTimespan = nTargetTimespan/4; if (nActualTimespan > nTargetTimespan*4) @@ -780,10 +780,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast) bnNew = bnProofOfWorkLimit; /// debug print - printf("\n\n\nGetNextWorkRequired RETARGET *****\n"); - printf("nTargetTimespan = %d nActualTimespan = %d\n", nTargetTimespan, nActualTimespan); - printf("Before: %08x %s\n", pindexLast->nBits, CBigNum().SetCompact(pindexLast->nBits).getuint256().ToString().c_str()); - printf("After: %08x %s\n", bnNew.GetCompact(), bnNew.getuint256().ToString().c_str()); + if (LogAcceptsCategory("net")) { printf("\n\n\nGetNextWorkRequired RETARGET *****\n"); printf("nTargetTimespan = %d nActualTimespan = %d\n", nTargetTimespan, nActualTimespan); printf("Before: %08x %s\n", pindexLast->nBits, CBigNum().SetCompact(pindexLast->nBits).getuint256().ToString().c_str()); printf("After: %08x %s\n", bnNew.GetCompact(), bnNew.getuint256().ToString().c_str()); } return bnNew.GetCompact(); } @@ -1037,7 +1034,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex) bool Reorganize(CTxDB& txdb, CBlockIndex* pindexNew) { - printf("*** REORGANIZE ***\n"); + LogPrint("net", "*** REORGANIZE ***\n"); // Find the fork CBlockIndex* pfork = pindexBest; @@ -1712,7 +1709,7 @@ bool ProcessMessages(CNode* pfrom) CDataStream& vRecv = pfrom->vRecv; if (vRecv.empty()) return true; - printf("ProcessMessages(%d bytes)\n", vRecv.size()); + LogPrint("net", "ProcessMessages(%d bytes)\n", vRecv.size()); // // Message format @@ -1730,13 +1727,13 @@ bool ProcessMessages(CNode* pfrom) { if (vRecv.size() > sizeof(CMessageHeader)) { - printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n"); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n"); vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader)); } break; } if (pstart - vRecv.begin() > 0) - printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); vRecv.erase(vRecv.begin(), pstart); // Read header @@ -1744,7 +1741,7 @@ bool ProcessMessages(CNode* pfrom) vRecv >> hdr; if (!hdr.IsValid()) { - printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); continue; } string strCommand = hdr.GetCommand(); @@ -1755,7 +1752,7 @@ bool ProcessMessages(CNode* pfrom) { // Rewind and wait for rest of message ///// need a mechanism to give up waiting for overlong message size error - printf("MESSAGE-BREAK 2\n"); + if (LogAcceptsCategory("net")) printf("MESSAGE-BREAK 2\n"); vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr)); Sleep(100); break; @@ -1776,7 +1773,7 @@ bool ProcessMessages(CNode* pfrom) } CATCH_PRINT_EXCEPTION("ProcessMessage()") if (!fRet) - printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str()); } vRecv.Compact(); @@ -1789,13 +1786,10 @@ bool ProcessMessages(CNode* pfrom) bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { static map > mapReuseKey; - printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); - for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) - printf("%02x ", vRecv[i] & 0xff); - printf("\n"); + if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { - printf("dropmessages DROPPING RECV MESSAGE\n"); + if (LogAcceptsCategory("net")) printf("dropmessages DROPPING RECV MESSAGE\n"); return true; } @@ -1833,7 +1827,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); } - printf("version addrMe = %s\n", addrMe.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); } @@ -1844,30 +1838,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "addr") - { - vector vAddr; - vRecv >> vAddr; - - // Store the new addresses - CAddrDB addrdb; - foreach(const CAddress& addr, vAddr) - { - if (fShutdown) - return true; - if (AddAddress(addrdb, addr)) - { - // Put on lists to send to other nodes - pfrom->setAddrKnown.insert(addr); - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (!pnode->setAddrKnown.count(addr)) - pnode->vAddrToSend.push_back(addr); - } - } - } - - else if (strCommand == "inv") { vector vInv; @@ -1881,7 +1851,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->AddInventoryKnown(inv); bool fAlreadyHave = AlreadyHave(txdb, inv); - printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new"); + if (LogAcceptsCategory("net")) printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new"); if (!fAlreadyHave) pfrom->AskFor(inv); @@ -1900,7 +1870,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { if (fShutdown) return true; - printf("received getdata for: %s\n", inv.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("received getdata for: %s\n", inv.ToString().c_str()); if (inv.type == MSG_BLOCK) { @@ -1940,12 +1910,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) // Send the rest of the chain if (pindex) pindex = pindex->pnext; - printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str()); + if (LogAcceptsCategory("net")) printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str()); for (; pindex; pindex = pindex->pnext) { if (pindex->GetBlockHash() == hashStop) { - printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str()); + if (LogAcceptsCategory("net")) printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str()); break; } @@ -1997,7 +1967,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (tx.AcceptTransaction(true)) { - printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); AddToWalletIfMine(tx, NULL); RelayMessage(inv, vMsg); mapAlreadyAskedFor.erase(inv); @@ -2011,7 +1981,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } else if (fMissingInputs) { - printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); AddOrphanTx(vMsg); } } @@ -2041,7 +2011,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) vRecv >> *pblock; //// debug print - printf("received block:\n"); pblock->print(); + if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); } CInv inv(MSG_BLOCK, pblock->GetHash()); pfrom->AddInventoryKnown(inv); @@ -2051,25 +2021,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "getaddr") - { - pfrom->vAddrToSend.clear(); - //// need to expand the time range if not enough found - int64 nSince = GetAdjustedTime() - 60 * 60; // in the last hour - CRITICAL_BLOCK(cs_mapAddresses) - { - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - { - if (fShutdown) - return true; - const CAddress& addr = item.second; - if (addr.nTime > nSince) - pfrom->vAddrToSend.push_back(addr); - } - } - } - - else if (strCommand == "checkorder") { uint256 hashReply; @@ -2134,12 +2085,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) else { // Ignore unknown commands for extensibility - printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str()); } if (!vRecv.empty()) - printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); return true; } @@ -2162,19 +2113,6 @@ bool SendMessages(CNode* pto) return true; - // - // Message: addr - // - vector vAddrToSend; - vAddrToSend.reserve(pto->vAddrToSend.size()); - foreach(const CAddress& addr, pto->vAddrToSend) - if (!pto->setAddrKnown.count(addr)) - vAddrToSend.push_back(addr); - pto->vAddrToSend.clear(); - if (!vAddrToSend.empty()) - pto->PushMessage("addr", vAddrToSend); - - // // Message: inventory // @@ -2204,7 +2142,7 @@ bool SendMessages(CNode* pto) while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow) { const CInv& inv = (*pto->mapAskFor.begin()).second; - printf("sending getdata: %s\n", inv.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); if (!AlreadyHave(txdb, inv)) vAskFor.push_back(inv); pto->mapAskFor.erase(pto->mapAskFor.begin()); @@ -2280,13 +2218,21 @@ void BlockSHA256(const void* pin, unsigned int nBlocks, void* pout) // --------------------------------------------------------------------------- -// Participant pool miner — connects to an external Stratum server, receives +// Participant pool miner -- connects to an external Stratum server, receives // RandomX jobs, hashes with the local CPU, and submits shares. // Called from BitcoinMiner() when nMineMode == MINE_PARTICIPANT. // --------------------------------------------------------------------------- using json = nlohmann::json; + +// --------------------------------------------------------------------------- +// Participant miner -- connects to a pool over a .btf tunnel, runs Stratum, +// hashes with RandomX, submits shares. Called from BitcoinMiner(). +// --------------------------------------------------------------------------- + +using json = nlohmann::json; + static std::string ToHexStr(const void* p, size_t n) { const unsigned char* b = (const unsigned char*)p; @@ -2300,10 +2246,10 @@ static std::vector FromHexStr(const std::string& s) { std::vector v; for (size_t i = 0; i+1 < s.size(); i += 2) { - auto h=[](char c)->int{ - if(c>='0'&&c<='9')return c-'0'; - if(c>='a'&&c<='f')return c-'a'+10; - if(c>='A'&&c<='F')return c-'A'+10; + auto h = [](char c)->int{ + if(c>='0'&&c<='9') return c-'0'; + if(c>='a'&&c<='f') return c-'a'+10; + if(c>='A'&&c<='F') return c-'A'+10; return 0; }; v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1]))); @@ -2313,14 +2259,13 @@ static std::vector FromHexStr(const std::string& s) static void UpdateParticipantHashRate(uint64 hashesSinceSample, int64 sampleStart) { - int64 now = GetTime(); - int64 elapsed = now - sampleStart; - if (elapsed <= 0) - elapsed = 1; - double rate = (double)hashesSinceSample / (double)elapsed; - gParticipantHashRateX1000.store((uint64)(rate * 1000.0)); + int64 elapsed = GetTime() - sampleStart; + if (elapsed <= 0) elapsed = 1; + gParticipantHashRateX1000.store( + (uint64)((double)hashesSinceSample / (double)elapsed * 1000.0)); } +// Blocking send of a JSON line to the pool. static bool StratumSendLine(SOCKET s, const json& j) { std::string line = j.dump() + "\n"; @@ -2333,38 +2278,59 @@ static bool StratumSendLine(SOCKET s, const json& j) return true; } -static bool StratumRecvLine(SOCKET s, std::string& buf, std::string& line) +// Non-blocking recv into carry buffer. Returns true + fills line when a +// complete newline-terminated message is ready. Returns false in two cases: +// disconnected=true -- socket closed by peer; caller must stop +// disconnected=false -- no complete line yet; caller should select() and retry +static bool StratumRecvLine(SOCKET s, std::string& buf, + std::string& line, bool& disconnected) { - while (true) { - size_t pos = buf.find('\n'); - if (pos != std::string::npos) { - line = buf.substr(0, pos); - if (!line.empty() && line.back()=='\r') line.pop_back(); - buf = buf.substr(pos+1); - return true; - } - char tmp[4096]; - int r = recv(s, tmp, sizeof(tmp)-1, 0); - if (r <= 0) return false; - tmp[r] = 0; - buf += tmp; + disconnected = false; + char tmp[4096]; +#ifdef _WIN32 + u_long avail = 0; + ioctlsocket(s, FIONREAD, &avail); + if (avail > 0) { + int r = recv(s, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + } +#else + { + int r = recv(s, tmp, sizeof(tmp)-1, MSG_DONTWAIT); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + // r < 0: EAGAIN -- no data right now } +#endif + size_t pos = buf.find('\n'); + if (pos == std::string::npos) return false; + line = buf.substr(0, pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + buf = buf.substr(pos + 1); + return true; } +// How long to wait for a mining.notify after authorize before giving up +static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; + static bool PoolParticipantMiner() { - printf("PoolParticipantMiner: connecting to %s\n", strParticipantPool.c_str()); - SetParticipantMiningStatus("resolving pool address"); - + // ------------------------------------------------------------------ + // Step 1: resolve .btf address -> meeting node + // ------------------------------------------------------------------ if (strParticipantPool.empty()) { - printf("PoolParticipantMiner: no pool selected\n"); + LogPrint("worker", "[worker] no pool selected\n"); SetParticipantMiningStatus("no pool selected"); return false; } + LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str()); + SetParticipantMiningStatus("resolving pool address"); + unsigned char target_pubkey[32]; if (!btf::ParseAddress(strParticipantPool, target_pubkey)) { - printf("PoolParticipantMiner: invalid .btf address %s\n", strParticipantPool.c_str()); + LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str()); SetParticipantMiningStatus("invalid pool address"); return false; } @@ -2372,35 +2338,48 @@ static bool PoolParticipantMiner() std::string meetingHostPort; unsigned char service_enc_pub[32]; if (!BtfResolve(strParticipantPool, meetingHostPort, service_enc_pub)) { - printf("PoolParticipantMiner: failed to resolve %s\n", strParticipantPool.c_str()); + LogPrint("worker", "[worker] failed to resolve pool %s" + " (no Nostr descriptor found -- pool may be offline or relays down)\n", + strParticipantPool.c_str()); SetParticipantMiningStatus("failed to resolve pool"); return false; } + LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); size_t colon = meetingHostPort.rfind(':'); if (colon == std::string::npos) { - printf("PoolParticipantMiner: bad meeting node %s\n", meetingHostPort.c_str()); + LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); SetParticipantMiningStatus("bad pool endpoint"); return false; } std::string host = meetingHostPort.substr(0, colon); int port = atoi(meetingHostPort.substr(colon + 1).c_str()); if (port <= 0 || port > 65535) { - printf("PoolParticipantMiner: bad meeting port %d\n", port); + LogPrint("worker", "[worker] bad meeting port %d in '%s'\n", + port, meetingHostPort.c_str()); SetParticipantMiningStatus("bad pool port"); return false; } - btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, target_pubkey, service_enc_pub); + // ------------------------------------------------------------------ + // Step 2: open encrypted tunnel to pool + // ------------------------------------------------------------------ + SetParticipantMiningStatus("opening tunnel"); + btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, + target_pubkey, service_enc_pub); if (s == INVALID_SOCKET) { - printf("PoolParticipantMiner: tunnel connect failed via %s\n", meetingHostPort.c_str()); + LogPrint("worker", "[worker] tunnel connect FAILED via %s" + " (relay unreachable or pool not listening)\n", + meetingHostPort.c_str()); SetParticipantMiningStatus("tunnel connect failed"); return false; } - printf("PoolParticipantMiner: connected via %s\n", meetingHostPort.c_str()); - SetParticipantMiningStatus("tunnel connected"); + LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); + SetParticipantMiningStatus("tunnel open"); - // Use our wallet address as mining username so payouts go to our wallet + // ------------------------------------------------------------------ + // Step 3: determine our payout address (mining username) + // ------------------------------------------------------------------ std::string username; { std::vector vchPubKey; @@ -2408,102 +2387,260 @@ static bool PoolParticipantMiner() username = PubKeyToAddress(vchPubKey); } if (username.empty()) username = BtfLocalAddress(); - if (username.empty()) username = "unknown"; - + if (username.empty()) { + LogPrint("worker", "[worker] WARNING: could not determine wallet address" + " -- shares will not pay out\n"); + username = "unknown"; + } + LogPrint("worker", "[worker] payout address: %s\n", username.c_str()); + + // ------------------------------------------------------------------ + // Step 4: build the fast (~2 GB dataset) RandomX mode if not already + // built, then start our VM. Without this, RandomXCreateMinerVM() falls + // back to "light" mode (cache only) forever, which is dramatically + // slower per hash than fast mode -- it recomputes dataset items on the + // fly for every single hash instead of a direct memory lookup. That + // alone can turn a "few seconds per share" pool into "many minutes per + // share" regardless of how easy the share target is. + // + // This MUST happen before we send mining.subscribe/authorize, not + // after: it's a synchronous, single-threaded-per-call build that can + // take anywhere from several seconds to a couple of minutes depending + // on core count. If it runs after the handshake messages are sent, + // the pool's replies (subscribe ack, authorize ack, set_difficulty, + // and the first job) all land in the OS socket buffer while we're + // not reading -- nothing is lost, but we sit there looking (and + // logging) like we're "stuck waiting for pool response" for the + // entire build duration, when really we're just not listening yet. + // Building the dataset first means that by the time we say + // "subscribing", we're genuinely ready to read the reply the instant + // it arrives. + // ------------------------------------------------------------------ + if (!RandomXFastReady()) { + SetParticipantMiningStatus("building RandomX dataset (~2GB, one-time)"); + unsigned int nThreads = std::thread::hardware_concurrency(); + RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); + } + void* rxvm = RandomXCreateMinerVM(); + if (!rxvm) { + LogPrint("worker", "[worker] failed to create RandomX VM\n"); + SetParticipantMiningStatus("RandomX init failed"); + closesocket(s); return false; + } + LogPrint("worker", "[worker] RandomX VM ready (%s)\n", + RandomXFastReady() ? "fast 2GB" : "light 256MB"); + + // ------------------------------------------------------------------ + // Step 5: Stratum subscribe + authorize. The socket is read from + // immediately after this, in the main loop below, so nothing sits + // unread behind a blocking dataset build anymore. + // ------------------------------------------------------------------ std::string buf; int msgId = 1; std::set pendingSubmitIds; - // Subscribe SetParticipantMiningStatus("subscribing"); - if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"},{"params",json::array()}})) - { SetParticipantMiningStatus("subscribe failed"); closesocket(s); return false; } + LogPrint("worker", "[worker->pool] sending mining.subscribe\n"); + if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"}, + {"params",json::array()}})) { + LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n"); + SetParticipantMiningStatus("subscribe send failed"); + RandomXDestroyMinerVM(rxvm); + closesocket(s); return false; + } - // Authorize SetParticipantMiningStatus("authorizing"); + LogPrint("worker", "[worker->pool] sending mining.authorize as %s\n", username.c_str()); if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.authorize"}, - {"params",json::array({username,"x"})}})) - { SetParticipantMiningStatus("authorize failed"); closesocket(s); return false; } + {"params",json::array({username,"x"})}})) { + LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n"); + SetParticipantMiningStatus("authorize send failed"); + RandomXDestroyMinerVM(rxvm); + closesocket(s); return false; + } - // Job state + // ------------------------------------------------------------------ + // Step 6: main loop -- receive messages, hash, submit shares + // ------------------------------------------------------------------ std::string jobId; - unsigned char header[80]; + unsigned char header[80] = {}; uint256 target; - bool haveJob = false; + double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool + bool haveJob = false; bool gotSubscribeAck = false; bool gotAuthorizeAck = false; - int64 authWaitStart = 0; - unsigned int nNonce = 1; - uint64 hashesSinceSample = 0; - int64 sampleStart = GetTime(); + int64 authWaitStart = 0; + int64 lastStuckLog = 0; + unsigned int nNonce = 1; + uint64 hashesSinceSample = 0; + uint64 sessionSharesSent = 0; // this connection only + uint64 sessionSharesAccepted = 0; + int64 sampleStart = GetTime(); - void* rxvm = RandomXCreateMinerVM(); - if (!rxvm) { SetParticipantMiningStatus("failed to start RandomX miner"); closesocket(s); return false; } - printf("PoolParticipantMiner: RandomX VM ready (%s)\n", - RandomXFastReady() ? "fast 2GB" : "light 256MB"); - SetParticipantMiningStatus("authorized; asking pool for work"); + SetParticipantMiningStatus("waiting for pool response"); while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { - // Non-blocking receive check + // -- Receive: check buffer first, then select -- { - fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds); - struct timeval tv={0,50000}; // 50ms - if (select((int)s+1, &fds, NULL, NULL, &tv) > 0) { + bool haveBuffered = buf.find('\n') != std::string::npos; + bool readable = false; + if (!haveBuffered) { + fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds); + struct timeval tv = {0, 50000}; // 50ms + readable = select((int)s+1, &fds, NULL, NULL, &tv) > 0; + } + + if (haveBuffered || readable) { std::string line; - if (!StratumRecvLine(s, buf, line)) break; - try { - json msg = json::parse(line); - if (msg.contains("id") && msg["id"].is_number_integer() && msg.contains("result")) { - int id = msg["id"].get(); - if (id == 1 && msg["result"].is_boolean() && msg["result"].get()) { - gotSubscribeAck = true; - SetParticipantMiningStatus("subscribed; authorizing"); - } else if (id == 2 && msg["result"].is_boolean()) { - gotAuthorizeAck = msg["result"].get(); - authWaitStart = GetTime(); - SetParticipantMiningStatus(gotAuthorizeAck ? "authorized; asking pool for work" : "authorization rejected"); + bool disc = false; + while (StratumRecvLine(s, buf, line, disc)) { + json msg; + try { msg = json::parse(line); } catch (...) { + LogPrint("worker", "[pool->worker] JSON parse error: %.80s\n", + line.c_str()); + continue; + } + + // -- Response to our subscribe (id=1) -- + if (msg.contains("id") && msg["id"].is_number_integer() + && msg["id"].get() == 1 + && msg.contains("result") && !msg["result"].is_null()) { + gotSubscribeAck = true; + LogPrint("worker", "[pool->worker] subscribe ack received\n"); + SetParticipantMiningStatus("subscribed; waiting for authorize ack"); + } + + // -- Response to our authorize (id=2) -- + else if (msg.contains("id") && msg["id"].is_number_integer() + && msg["id"].get() == 2 + && msg.contains("result")) { + bool ok = msg["result"].is_boolean() && msg["result"].get(); + authWaitStart = GetTime(); + if (ok) { + gotAuthorizeAck = true; + LogPrint("worker", "[pool->worker] authorize OK -- waiting for job\n"); + SetParticipantMiningStatus("authorized; waiting for job"); + } else { + std::string reason = msg.value("error", json("unspecified")).is_string() + ? msg["error"].get() : "unspecified"; + LogPrint("worker", "[pool->worker] authorize REJECTED: %s\n", + reason.c_str()); + SetParticipantMiningStatus("authorization rejected"); + // Don't break -- pool might still send a job (some pools + // reject bad addresses but still send work) } - if (pendingSubmitIds.erase(id) > 0) { - if (msg["result"].is_boolean() && msg["result"].get()) { + } + + // -- Share submission response -- + else if (msg.contains("id") && msg["id"].is_number_integer()) { + int rid = msg["id"].get(); + if (pendingSubmitIds.erase(rid) > 0) { + bool accepted = msg.contains("result") + && msg["result"].is_boolean() + && msg["result"].get(); + if (accepted) { gParticipantSharesAccepted.fetch_add(1); + sessionSharesAccepted++; + LogPrint("worker", "[pool->worker] share id=%d ACCEPTED\n", rid); SetParticipantMiningStatus("share accepted"); } else { - SetParticipantMiningStatus("share rejected"); + std::string reason = (msg.contains("error") + && msg["error"].is_string()) + ? msg["error"].get() : "unspecified"; + LogPrint("worker", "[pool->worker] share id=%d REJECTED: %s\n", + rid, reason.c_str()); + SetParticipantMiningStatus("share rejected: " + reason); } } } - std::string method = msg.value("method",""); - if (method == "mining.notify") { - auto& p = msg["params"]; - jobId = p[0].get(); - // p[1] = full 80-byte header hex - auto hdrBytes = FromHexStr(p[1].get()); - if (hdrBytes.size() >= 80) - memcpy(header, hdrBytes.data(), 80); - // p[3] = share target hex (little-endian uint256) - auto tgtBytes = FromHexStr(p[3].get()); - if (tgtBytes.size() >= 32) - memcpy(&target, tgtBytes.data(), 32); - haveJob = true; - nNonce = 1; - SetParticipantMiningStatus("job received; hashing"); - printf("PoolParticipantMiner: new job %s\n", jobId.c_str()); - } else if (method == "mining.set_difficulty") { - // acknowledged, we use the target from notify + + // -- Server notification -- + else { + std::string method = msg.value("method", ""); + + if (method == "mining.notify") { + auto& p = msg["params"]; + if (!p.is_array() || p.size() < 4) { + LogPrint("worker", "[pool->worker] malformed mining.notify" + " (params size=%zu)\n", p.is_array() ? p.size() : 0); + } else { + std::string newJobId = p[0].get(); + auto hdrBytes = FromHexStr(p[1].get()); + auto tgtBytes = FromHexStr(p[3].get()); + + if (hdrBytes.size() < 80) { + LogPrint("worker", "[pool->worker] mining.notify:" + " header too short (%zu bytes)\n", + hdrBytes.size()); + } else if (tgtBytes.size() < 32) { + LogPrint("worker", "[pool->worker] mining.notify:" + " target too short (%zu bytes)\n", + tgtBytes.size()); + } else { + jobId = newJobId; + memcpy(header, hdrBytes.data(), 80); + memcpy(&target, tgtBytes.data(), 32); + haveJob = true; + nNonce = 1; + LogPrint("worker", "[pool->worker] new job %s received\n", + jobId.c_str()); + SetParticipantMiningStatus("hashing job " + jobId); + } + } + } else if (method == "mining.set_difficulty") { + // Pool adjusts our share difficulty via vardiff. + // Apply it immediately so we start hashing at the new target. + if (msg.contains("params") && msg["params"].is_array() + && msg["params"].size() > 0 + && msg["params"][0].is_number()) { + double newDiff = msg["params"][0].get(); + if (newDiff > 0.0) { + bool changed = (newDiff != currentDifficulty); + currentDifficulty = newDiff; + LogPrint("worker", "[pool->worker] set_difficulty %.6f%s\n", + newDiff, changed ? " (updated)" : " (initial)"); + } + } else { + LogPrint("worker", "[pool->worker] set_difficulty" + " received (no valid params)\n"); + } + } else if (!method.empty()) { + LogPrint("worker", "[pool->worker] unknown server method: %s\n", + method.c_str()); + } } - } catch(...) {} + } + + if (disc) { + LogPrint("worker", "[worker] pool closed the connection" + " (gotSubscribeAck=%d gotAuthorizeAck=%d haveJob=%d)\n", + gotSubscribeAck, gotAuthorizeAck, haveJob); + break; + } } } - if (gotAuthorizeAck && !haveJob && authWaitStart > 0 && GetTime() - authWaitStart > 10) { - SetParticipantMiningStatus("authorized; waiting for pool notify"); - authWaitStart = 0; + // -- Stuck watchdog: authorized but no job arriving -- + if (gotAuthorizeAck && !haveJob && authWaitStart > 0) { + int64 waited = GetTime() - authWaitStart; + if (waited > 10 && GetTime() - lastStuckLog >= 15) { + LogPrint("worker", "[worker] authorized but no job after %llds" + " -- pool tunnel is live, waiting on mining.notify\n", + (long long)waited); + lastStuckLog = GetTime(); + } + if (waited > STUCK_NO_JOB_TIMEOUT_SECS) { + LogPrint("worker", "[worker] no job received after %llds -- disconnecting" + " and reconnecting\n", (long long)STUCK_NO_JOB_TIMEOUT_SECS); + SetParticipantMiningStatus("timeout waiting for job; reconnecting"); + break; + } } if (!haveJob) { Sleep(100); continue; } - // Hash one nonce + // -- Hash one nonce -- memcpy(header+76, &nNonce, 4); uint256 hash = RandomXHashWithVM(rxvm, header, 80); hashesSinceSample++; @@ -2512,38 +2649,59 @@ static bool PoolParticipantMiner() UpdateParticipantHashRate(hashesSinceSample, sampleStart); if (hash <= target) { - printf("PoolParticipantMiner: share found! nonce=%u\n", nNonce); + LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n", + nNonce, jobId.c_str(), currentDifficulty); SetParticipantMiningStatus("share found; submitting"); int submitId = msgId++; pendingSubmitIds.insert(submitId); gParticipantSharesSent.fetch_add(1); + sessionSharesSent++; json submit = {{"id",submitId},{"method","mining.submit"}, - {"params",json::array({username, jobId, - ToHexStr(&nNonce,4)})}}; - if (!StratumSendLine(s, submit)) break; + {"params",json::array({username, jobId, ToHexStr(&nNonce,4)})}}; + if (!StratumSendLine(s, submit)) { + LogPrint("worker", "[worker] share submit send FAILED" + " -- pool connection lost\n"); + break; + } } nNonce++; - if (nNonce == 0) { haveJob = false; } // exhausted, wait for new job + if (nNonce == 0) { + LogPrint("worker", "[worker] nonce space exhausted for job %s" + " -- waiting for new job\n", jobId.c_str()); + haveJob = false; + } } + // ------------------------------------------------------------------ + // Cleanup + // ------------------------------------------------------------------ RandomXDestroyMinerVM(rxvm); closesocket(s); UpdateParticipantHashRate(hashesSinceSample, sampleStart); - SetParticipantMiningStatus("participant miner stopped"); - printf("PoolParticipantMiner: disconnected\n"); + SetParticipantMiningStatus("disconnected"); + LogPrint("worker", "[worker] disconnected from pool" + " (hashes this session: %llu, shares sent: %llu, accepted: %llu)\n", + (unsigned long long)hashesSinceSample, + (unsigned long long)sessionSharesSent, + (unsigned long long)sessionSharesAccepted); return true; } bool BitcoinMiner() { + // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here + // since the GUI hides Start Mining for Relay, but guard it regardless.) + if (nMineMode == MINE_RELAY) + return true; + // Participant mode: connect to external pool instead of mining solo/operator if (nMineMode == MINE_PARTICIPANT) { while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { PoolParticipantMiner(); if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { - printf("PoolParticipantMiner: reconnecting in 10s\n"); + LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n"); for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); } } @@ -2554,9 +2712,16 @@ bool BitcoinMiner() if (nMineMode == MINE_OPERATOR) return true; - printf("BitcoinMiner started\n"); + LogPrint("net", "BitcoinMiner started\n"); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); + // Build the fast (~2 GB dataset) RandomX mode once, up front, so we're + // not stuck mining in the much slower light/cache-only mode forever. + if (!RandomXFastReady()) { + unsigned int nThreads = std::thread::hardware_concurrency(); + RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); + } + CKey key; key.MakeNewKey(); CBigNum bnExtraNonce = 0; @@ -2639,7 +2804,7 @@ bool BitcoinMiner() } pblock->nBits = nBits; pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nFees); - printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); + if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); // @@ -2658,11 +2823,11 @@ bool BitcoinMiner() void* rxvm = RandomXCreateMinerVM(); if (!rxvm) { - printf("BitcoinMiner: failed to create RandomX VM\n"); + LogPrint("net", "BitcoinMiner: failed to create RandomX VM\n"); Sleep(1000); continue; } - printf("BitcoinMiner: mining with RandomX (%s)\n", + LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n", RandomXFastReady() ? "fast 2GB" : "light 256MB"); unsigned int nStart = GetTime(); @@ -2675,9 +2840,9 @@ bool BitcoinMiner() if (hash <= hashTarget) { //// debug print - printf("BitcoinMiner:\n"); - printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); - pblock->print(); + if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); + if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); + if (LogAcceptsCategory("net")) pblock->print(); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); CRITICAL_BLOCK(cs_main) @@ -2692,7 +2857,7 @@ bool BitcoinMiner() // Process this block the same as if we had received it from another node if (!ProcessBlock(NULL, pblock.release())) - printf("ERROR in BitcoinMiner, ProcessBlock, block not accepted\n"); + LogPrint("net", "ERROR in BitcoinMiner, ProcessBlock, block not accepted\n"); } SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); @@ -2860,11 +3025,11 @@ bool SelectCoins(int64 nTargetValue, set& setCoinsRet) setCoinsRet.insert(vValue[i].second); //// debug print - printf("SelectCoins() best subset: "); + if (LogAcceptsCategory("net")) printf("SelectCoins() best subset: "); for (int i = 0; i < vValue.size(); i++) if (vfBest[i]) - printf("%s ", FormatMoney(vValue[i].first).c_str()); - printf("total %s\n", FormatMoney(nBest).c_str()); + if (LogAcceptsCategory("net")) printf("%s ", FormatMoney(vValue[i].first).c_str()); + if (LogAcceptsCategory("net")) printf("total %s\n", FormatMoney(nBest).c_str()); } return true; @@ -2953,6 +3118,34 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in return true; } +// Undo what CommitTransactionSpent did: unmark the inputs it marked spent +// and erase the half-committed wallet entry. Used when a transaction is +// accepted into the wallet but then rejected by the mempool/chain, so we +// don't end up with a wallet that thinks money is spent that never left. +void RollbackTransactionSpent(const CWalletTx& wtxNew) +{ + CRITICAL_BLOCK(cs_main) + CRITICAL_BLOCK(cs_mapWallet) + { + set setCoins; + foreach(const CTxIn& txin, wtxNew.vin) + { + map::iterator mi = mapWallet.find(txin.prevout.hash); + if (mi != mapWallet.end()) + setCoins.insert(&mi->second); + } + foreach(CWalletTx* pcoin, setCoins) + { + pcoin->fSpent = false; + pcoin->WriteToDisk(); + vWalletUpdated.push_back(make_pair(pcoin->GetHash(), false)); + } + + EraseFromWallet(wtxNew.GetHash()); + } + MainFrameRepaint(); +} + // Call after CreateTransaction unless you want to abort bool CommitTransactionSpent(const CWalletTx& wtxNew) { @@ -3003,13 +3196,17 @@ bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) return error("SendMoney() : Error finalizing transaction"); } - printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); // Broadcast if (!wtxNew.AcceptTransaction()) { - // This must not fail. The transaction has already been signed and recorded. - throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n"); + // The transaction was already signed and committed to the wallet + // above, but the chain/mempool just rejected it (e.g. a raced + // input already spent by another transaction). Unwind the + // wallet state we just wrote instead of crashing the app. + RollbackTransactionSpent(wtxNew); + return error("SendMoney() : wtxNew.AcceptTransaction() failed"); } wtxNew.RelayWalletTransaction(); } diff --git a/src/main.h b/src/main.h index 3d7d6dd..8bb8609 100644 --- a/src/main.h +++ b/src/main.h @@ -48,13 +48,17 @@ extern CBlockIndex* pindexBest; extern unsigned int nTransactionsUpdated; extern string strSetDataDir; extern int nDropMessagesTest; -// Mining mode — persisted in wallet.dat -// MINE_SOLO (0): mine to own wallet, no pool server -// MINE_OPERATOR (1): run pool server, mine to own wallet, distribute to miners -// MINE_PARTICIPANT (2): connect to external pool, submit shares, receive payouts -#define MINE_SOLO 0 -#define MINE_OPERATOR 1 -#define MINE_PARTICIPANT 2 +// Mining mode -- persisted in wallet.dat +// Mining mode -- decided fresh every launch (CLI flags, else default). Never +// restored from wallet.dat; see LoadWallet() in db.cpp. +// MINE_RELAY (0): default. Node runs/syncs, never mines, no pool server. +// MINE_SOLO (1): mine to own wallet, no pool server +// MINE_OPERATOR (2): run pool server, mine to own wallet, distribute to miners +// MINE_PARTICIPANT (3): connect to external pool, submit shares, receive payouts +#define MINE_RELAY 0 +#define MINE_SOLO 1 +#define MINE_OPERATOR 2 +#define MINE_PARTICIPANT 3 extern int nMineMode; extern string strParticipantPool; // participant: pool .btf address extern string strPoolName; // operator: announced pool name @@ -78,6 +82,7 @@ struct PoolWorkerStatView uint64 totalShares; uint64 roundShares; int64 lastSeen; + double hashRate; // estimated H/s, derived from share difficulty + submit rate }; // Settings @@ -102,11 +107,11 @@ void RelayWalletTransactions(); bool LoadBlockIndex(bool fAllowNew=true); void PrintBlockTree(); bool BitcoinMiner(); -void ThreadRPCServer(void* parg); // rpc.cpp — .btf pool server +void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); void SetParticipantMiningStatus(const std::string& status); std::string GetParticipantMiningStatus(); -void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares); +void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate); void GetPoolWorkerStats(std::vector& out); void GetPendingPayouts(std::vector& out); bool ProcessMessages(CNode* pfrom); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index b7377da..842b0d5 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -1,4 +1,4 @@ -// Bitflash entry point — starts node threads then runs GUI (or headless). +// Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" #ifndef _WIN32 @@ -126,8 +126,6 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); - printf("Loading addresses...\n"); - if (!LoadAddresses()) printf("Warning: error loading addr.dat\n"); printf("Loading block index...\n"); string strErrors; if (!LoadBlockIndex()) { fprintf(stderr,"LoadBlockIndex failed\n"); return 1; } diff --git a/src/makefile.linux b/src/makefile.linux deleted file mode 100644 index 9e63f7d..0000000 --- a/src/makefile.linux +++ /dev/null @@ -1,55 +0,0 @@ -# Bitflash Linux build — GUI wallet + pool server + daemon mode. -# Requires: bash build-linux.sh (installs deps, builds secp256k1 + RandomX) -# Result: src/bitflash -# -# ./bitflash — GUI wallet -# ./bitflash /nogui /gen — headless daemon (same binary, no window) - -CXX ?= g++ - -SECP_INC ?= /usr/local/include -SECP_LIB ?= /usr/local/lib -RANDOMX_DIR ?= $(HOME)/RandomX - -WX_CFLAGS := $(shell wx-config --cxxflags) -WX_LIBS := $(shell wx-config --libs) - -CXXFLAGS = -std=gnu++14 -O2 -w -fpermissive -pthread \ - -DSECP256K1_STATIC \ - -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ - -I/usr/include/nlohmann \ - $(WX_CFLAGS) - -LIBS = -L$(SECP_LIB) -lsecp256k1 \ - -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ - -lboost_system \ - $(WX_LIBS) - -HEADERS = headers.h util.h main.h serialize.h uint256.h key.h bignum.h script.h \ - db.h base58.h net.h irc.h nostr.h randomx_pow.h btfaddr.h btfchan.h \ - btfrv.h btftunnel.h market.h compat.h uibase.h ui.h - -OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o obj/market.o \ - obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ - obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o \ - obj/rpc.o obj/uibase.o obj/ui.o - -all: bitflash - -obj/%.o: %.cpp $(HEADERS) - @mkdir -p obj - $(CXX) -c $(CXXFLAGS) -o $@ $< - -obj/sha.o: sha.cpp sha.h - @mkdir -p obj - $(CXX) -c $(CXXFLAGS) -O3 -o $@ $< - -bitflash: $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $(OBJS) $(LIBS) - @echo "Built: bitflash" - @echo " GUI: ./bitflash" - @echo " Daemon: ./bitflash /nogui /gen" - -clean: - rm -rf obj bitflash diff --git a/src/makefile.mingw b/src/makefile.mingw index d381653..7d35d92 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -28,13 +28,13 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ + obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ - key.h bignum.h script.h db.h base58.h net.h irc.h nostr.h \ + key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h all: bitflash.exe diff --git a/src/makefile.vc b/src/makefile.vc deleted file mode 100644 index 60dca78..0000000 --- a/src/makefile.vc +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2009 Satoshi Nakamoto -# Distributed under the MIT/X11 software license, see the accompanying -# file license.txt or http://www.opensource.org/licenses/mit-license.php. - - -!IF "$(BUILD)" != "debug" && "$(BUILD)" != "release" -BUILD=debug -!ENDIF -!IF "$(BUILD)" == "debug" -D=d -DEBUGFLAGS=/Zi /Od /D__WXDEBUG__ -!ENDIF - - - -INCLUDEPATHS=/I"/boost" /I"/DB/build_windows" /I"/OpenSSL/include" /I"/wxWidgets/lib/vc_lib/mswd" /I"/wxWidgets/include" -LIBPATHS=/LIBPATH:"/DB/build_windows/$(BUILD)" /LIBPATH:"/OpenSSL/out" /LIBPATH:"/wxWidgets/lib/vc_lib" -LIBS= \ - libdb47s$(D).lib \ - libeay32.lib \ - wxmsw28$(D)_richtext.lib wxmsw28$(D)_html.lib wxmsw28$(D)_core.lib wxbase28$(D).lib wxtiff$(D).lib wxjpeg$(D).lib wxpng$(D).lib wxzlib$(D).lib wxregex$(D).lib wxexpat$(D).lib \ - kernel32.lib user32.lib gdi32.lib comdlg32.lib winspool.lib winmm.lib shell32.lib comctl32.lib ole32.lib oleaut32.lib uuid.lib rpcrt4.lib advapi32.lib ws2_32.lib -WXDEFS=/DWIN32 /D__WXMSW__ /D_WINDOWS /DNOPCH -CFLAGS=/c /nologo /Ob0 /MD$(D) /EHsc /GR /Zm300 /YX /Fpobj/headers.pch $(DEBUGFLAGS) $(WXDEFS) $(INCLUDEPATHS) -HEADERS=headers.h util.h main.h serialize.h uint256.h key.h bignum.h script.h db.h base58.h - - - -all: bitcoin.exe - - -obj\util.obj: util.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\script.obj: script.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\db.obj: db.cpp $(HEADERS) market.h - cl $(CFLAGS) /Fo$@ %s - -obj\net.obj: net.cpp $(HEADERS) net.h - cl $(CFLAGS) /Fo$@ %s - -obj\main.obj: main.cpp $(HEADERS) net.h market.h - cl $(CFLAGS) /Fo$@ %s - -obj\market.obj: market.cpp $(HEADERS) market.h - cl $(CFLAGS) /Fo$@ %s - -obj\ui.obj: ui.cpp $(HEADERS) net.h uibase.h ui.h market.h - cl $(CFLAGS) /Fo$@ %s - -obj\uibase.obj: uibase.cpp uibase.h - cl $(CFLAGS) /Fo$@ %s - -obj\sha.obj: sha.cpp sha.h - cl $(CFLAGS) /O2 /Fo$@ %s - -obj\irc.obj: irc.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\ui.res: ui.rc rc/bitcoin.ico rc/check.ico rc/send16.bmp rc/send16mask.bmp rc/send16masknoshadow.bmp rc/send20.bmp rc/send20mask.bmp rc/addressbook16.bmp rc/addressbook16mask.bmp rc/addressbook20.bmp rc/addressbook20mask.bmp - rc $(INCLUDEPATHS) $(WXDEFS) /Fo$@ %s - - - -OBJS=obj\util.obj obj\script.obj obj\db.obj obj\net.obj obj\main.obj obj\market.obj \ - obj\ui.obj obj\uibase.obj obj\sha.obj obj\irc.obj obj\ui.res - -bitcoin.exe: $(OBJS) - -kill /f bitcoin.exe & sleep 1 - link /nologo /DEBUG /SUBSYSTEM:WINDOWS /OUT:$@ $(LIBPATHS) $** $(LIBS) - -clean: - -del /Q obj\* - -del *.ilk - -del *.pdb diff --git a/src/net.cpp b/src/net.cpp index bdfb9c3..ab07f94 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -12,7 +12,6 @@ void ThreadMessageHandler2(void* parg); void ThreadSocketHandler2(void* parg); -void ThreadOpenConnections2(void* parg); @@ -32,8 +31,6 @@ bool fShutdown = false; array vfThreadRunning; vector vNodes; CCriticalSection cs_vNodes; -map, CAddress> mapAddresses; -CCriticalSection cs_mapAddresses; map mapRelay; deque > vRelayExpiration; CCriticalSection cs_mapRelay; @@ -41,61 +38,6 @@ map mapAlreadyAskedFor; string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) - -CAddress addrProxy; - -bool ConnectSocket(const CAddress& addrConnect, SOCKET& hSocketRet) -{ - hSocketRet = INVALID_SOCKET; - - SOCKET hSocket = socket(AF_INET, SOCK_STREAM, 0); - if (hSocket == INVALID_SOCKET) - return false; - - bool fRoutable = !(addrConnect.GetByte(3) == 10 || (addrConnect.GetByte(3) == 192 && addrConnect.GetByte(2) == 168)); - bool fProxy = (addrProxy.ip && fRoutable); - struct sockaddr_in sockaddr = (fProxy ? addrProxy.GetSockAddr() : addrConnect.GetSockAddr()); - - if (connect(hSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR) - { - closesocket(hSocket); - return false; - } - - if (fProxy) - { - printf("Proxy connecting to %s\n", addrConnect.ToString().c_str()); - char pszSocks4IP[] = "\4\1\0\0\0\0\0\0user"; - memcpy(pszSocks4IP + 2, &addrConnect.port, 2); - memcpy(pszSocks4IP + 4, &addrConnect.ip, 4); - char* pszSocks4 = pszSocks4IP; - int nSize = sizeof(pszSocks4IP); - - int ret = send(hSocket, pszSocks4, nSize, 0); - if (ret != nSize) - { - closesocket(hSocket); - return error("Error sending to proxy\n"); - } - char pchRet[8]; - if (recv(hSocket, pchRet, 8, 0) != 8) - { - closesocket(hSocket); - return error("Error reading proxy response\n"); - } - if (pchRet[1] != 0x5a) - { - closesocket(hSocket); - return error("Proxy returned error %d\n", pchRet[1]); - } - printf("Proxy connection established %s\n", addrConnect.ToString().c_str()); - } - - hSocketRet = hSocket; - return true; -} - - bool GetMyExternalIP(unsigned int& ipRet) { // Try several plain-text IP echo services in order. @@ -178,38 +120,6 @@ bool GetMyExternalIP(unsigned int& ipRet) -bool AddAddress(CAddrDB& addrdb, const CAddress& addr) -{ - if (!addr.IsRoutable()) - return false; - if (addr.ip == addrLocalHost.ip) - return false; - CRITICAL_BLOCK(cs_mapAddresses) - { - map, CAddress>::iterator it = mapAddresses.find(addr.GetKey()); - if (it == mapAddresses.end()) - { - // New address - mapAddresses.insert(make_pair(addr.GetKey(), addr)); - addrdb.WriteAddress(addr); - return true; - } - else - { - CAddress& addrFound = (*it).second; - if ((addrFound.nServices | addr.nServices) != addrFound.nServices) - { - // Services have been added - addrFound.nServices |= addr.nServices; - addrdb.WriteAddress(addrFound); - return true; - } - } - } - return false; -} - - @@ -330,64 +240,6 @@ CNode* FindNode(unsigned int ip) return NULL; } -CNode* FindNode(CAddress addr) -{ - CRITICAL_BLOCK(cs_vNodes) - { - foreach(CNode* pnode, vNodes) - if (pnode->addr == addr) - return (pnode); - } - return NULL; -} - -CNode* ConnectNode(CAddress addrConnect, int64 nTimeout) -{ - if (addrConnect.ip == addrLocalHost.ip) - return NULL; - - // Look for an existing connection - CNode* pnode = FindNode(addrConnect.ip); - if (pnode) - { - if (nTimeout != 0) - pnode->AddRef(nTimeout); - else - pnode->AddRef(); - return pnode; - } - - if (fDebug) - printf("trying %s\n", addrConnect.ToString().c_str()); - - // Connect - SOCKET hSocket; - if (ConnectSocket(addrConnect, hSocket)) - { - if (fDebug) - printf("connected %s\n", addrConnect.ToString().c_str()); - - // Add node - CNode* pnode = new CNode(hSocket, addrConnect, false); - if (nTimeout != 0) - pnode->AddRef(nTimeout); - else - pnode->AddRef(); - CRITICAL_BLOCK(cs_vNodes) - vNodes.push_back(pnode); - - CRITICAL_BLOCK(cs_mapAddresses) - mapAddresses[addrConnect.GetKey()].nLastFailed = 0; - return pnode; - } - else - { - CRITICAL_BLOCK(cs_mapAddresses) - mapAddresses[addrConnect.GetKey()].nLastFailed = GetTime(); - return NULL; - } -} - // Synthetic marker address for a tunneled `.btf` peer. The real endpoint is // unknown by design (the tunnel hides it), so tag the CNode with a non-routable // 10.x.x.x address (fails IsRoutable, so it is never gossiped as a real peer) @@ -401,20 +253,15 @@ static CAddress BtfMarkerAddr(const unsigned char b5[5]) return CAddress(ip, port, nLocalServices); } -// Connect to a peer by its `.btf` address: resolve the self-certified -// descriptor over Nostr, then tunnel through its meeting node with the -// end-to-end channel. Neither side ever learns the other's IP. -CNode* ConnectNodeBtf(const string& strBtfAddr) +// Shared tail: given a peer's pubkey and already-resolved rendezvous +// coordinates, open the tunnel and register the CNode. Used by both +// ConnectNodeBtf (which resolves the descriptor itself) and +// ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it +// makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why +// that matters when dialing many candidates in parallel). +static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], + const string& strMeeting, const unsigned char enc_pub[32]) { - unsigned char pk[32]; - if (!btf::ParseAddress(strBtfAddr, pk)) - { - printf("ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); - return NULL; - } - if (BtfLocalAddress() == strBtfAddr) - return NULL; // ourselves - CAddress addr = BtfMarkerAddr(pk); CNode* pnode = FindNode(addr.ip); if (pnode) @@ -423,17 +270,6 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) return pnode; } - if (fDebug) - printf("trying %s\n", strBtfAddr.c_str()); - - string strMeeting; - unsigned char enc_pub[32]; - if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) - { - if (fDebug) - printf("ConnectNodeBtf: could not resolve a descriptor for %s\n", strBtfAddr.c_str()); - return NULL; - } size_t colon = strMeeting.rfind(':'); if (colon == string::npos) return NULL; @@ -446,12 +282,12 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) if (hSocket == INVALID_SOCKET) { if (fDebug) - printf("ConnectNodeBtf: tunnel to %s via %s failed\n", strBtfAddr.c_str(), strMeeting.c_str()); + LogPrint("net", "ConnectNodeBtf: tunnel to %s via %s failed\n", strBtfAddr.c_str(), strMeeting.c_str()); return NULL; } if (fDebug) - printf("connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); + LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); // Add node pnode = new CNode(hSocket, addr, false); @@ -461,6 +297,53 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) return pnode; } +// Connect to a peer by its `.btf` address: resolve the self-certified +// descriptor over Nostr, then tunnel through its meeting node with the +// end-to-end channel. Neither side ever learns the other's IP. +CNode* ConnectNodeBtf(const string& strBtfAddr) +{ + unsigned char pk[32]; + if (!btf::ParseAddress(strBtfAddr, pk)) + { + LogPrint("net", "ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); + return NULL; + } + if (BtfLocalAddress() == strBtfAddr) + return NULL; // ourselves + + if (fDebug) + LogPrint("net", "trying %s\n", strBtfAddr.c_str()); + + string strMeeting; + unsigned char enc_pub[32]; + if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) + { + if (fDebug) + LogPrint("net", "ConnectNodeBtf: could not resolve a descriptor for %s\n", strBtfAddr.c_str()); + return NULL; + } + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); +} + +// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller +// already resolved the descriptor (e.g. via BtfResolveMany, batching many +// addresses into one relay round-trip instead of one relay round-trip per +// address). Makes zero relay connections -- only talks to the peer's +// rendezvous meeting node. +CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, + const unsigned char enc_pub[32]) +{ + unsigned char pk[32]; + if (!btf::ParseAddress(strBtfAddr, pk)) + { + LogPrint("net", "ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); + return NULL; + } + if (BtfLocalAddress() == strBtfAddr) + return NULL; // ourselves + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); +} + // Anonymous inbound listener (this node's `.btf` hidden service). Registers at // the meeting relay and blocks until a client dials our address; each pairing // becomes a normal inbound CNode riding the end-to-end channel, then we @@ -535,7 +418,7 @@ void ThreadBtfAccept(void* parg) RAND_bytes(rnd, sizeof(rnd)); CAddress addr = BtfMarkerAddr(rnd); - printf("accepted .btf connection via rendezvous %s\n", strMeeting.c_str()); + LogPrint("net", "accepted .btf connection via rendezvous %s\n", strMeeting.c_str()); CNode* pnode = new CNode(hSocket, addr, true); pnode->AddRef(); CRITICAL_BLOCK(cs_vNodes) @@ -574,7 +457,7 @@ void ThreadBtfConnect(void* parg) void CNode::Disconnect() { - printf("disconnecting node %s\n", addr.ToString().c_str()); + LogPrint("net", "disconnecting node %s\n", addr.ToString().c_str()); closesocket(hSocket); @@ -653,7 +536,7 @@ void ThreadSocketHandler2(void* parg) if (pnodeExtra->GetRefCount() <= (pnodeExtra->fNetworkNode ? 1 : 0)) { - printf("(%d nodes) disconnecting duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str()); + LogPrint("net", "(%d nodes) disconnecting duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str()); if (pnodeExtra->fNetworkNode && !pnode->fNetworkNode) { pnode->AddRef(); @@ -745,7 +628,7 @@ void ThreadSocketHandler2(void* parg) if (nSelect == SOCKET_ERROR) { int nErr = WSAGetLastError(); - printf("select failed: %d\n", nErr); + LogPrint("net", "select failed: %d\n", nErr); for (int i = 0; i <= hSocketMax; i++) { FD_SET(i, &fdsetRecv); @@ -784,7 +667,7 @@ void ThreadSocketHandler2(void* parg) } else { - printf("accepted connection from %s\n", addr.ToString().c_str()); + LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); CNode* pnode = new CNode(hSocket, addr, true); pnode->AddRef(); CRITICAL_BLOCK(cs_vNodes) @@ -823,7 +706,7 @@ void ThreadSocketHandler2(void* parg) { // socket closed gracefully if (!pnode->fDisconnect) - printf("recv: socket closed\n"); + LogPrint("net", "recv: socket closed\n"); pnode->fDisconnect = true; } else if (nBytes < 0) @@ -833,7 +716,7 @@ void ThreadSocketHandler2(void* parg) if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) { if (!pnode->fDisconnect) - printf("recv failed: %d\n", nErr); + LogPrint("net", "recv failed: %d\n", nErr); pnode->fDisconnect = true; } } @@ -862,7 +745,7 @@ void ThreadSocketHandler2(void* parg) } else { - printf("send error %d\n", nBytes); + LogPrint("net", "send error %d\n", nBytes); if (pnode->ReadyToDisconnect()) pnode->vSend.clear(); } @@ -885,141 +768,6 @@ void ThreadSocketHandler2(void* parg) -void ThreadOpenConnections(void* parg) -{ - IMPLEMENT_RANDOMIZE_STACK(ThreadOpenConnections(parg)); - - loop - { - vfThreadRunning[1] = true; - CheckForShutdown(1); - try - { - ThreadOpenConnections2(parg); - } - CATCH_PRINT_EXCEPTION("ThreadOpenConnections()") - vfThreadRunning[1] = false; - Sleep(5000); - } -} - -void ThreadOpenConnections2(void* parg) -{ - printf("ThreadOpenConnections started\n"); - - // Initiate network connections - const int nMaxConnections = 15; - loop - { - // Wait - vfThreadRunning[1] = false; - Sleep(500); - while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size()) - { - CheckForShutdown(1); - Sleep(2000); - } - vfThreadRunning[1] = true; - CheckForShutdown(1); - - - // Make a list of unique class C's - unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 }; - unsigned int nIPCMask = *(unsigned int*)pchIPCMask; - vector vIPC; - CRITICAL_BLOCK(cs_mapAddresses) - { - vIPC.reserve(mapAddresses.size()); - unsigned int nPrev = 0; - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - { - const CAddress& addr = item.second; - if (!addr.IsIPv4()) - continue; - - // Taking advantage of mapAddresses being in sorted order, - // with IPs of the same class C grouped together. - unsigned int ipC = addr.ip & nIPCMask; - if (ipC != nPrev) - vIPC.push_back(nPrev = ipC); - } - } - - // - // The IP selection process is designed to limit vulnerability to address flooding. - // Any class C (a.b.c.?) has an equal chance of being chosen, then an IP is - // chosen within the class C. An attacker may be able to allocate many IPs, but - // they would normally be concentrated in blocks of class C's. They can hog the - // attention within their class C, but not the whole IP address space overall. - // A lone node in a class C will get as much attention as someone holding all 255 - // IPs in another class C. - // - bool fSuccess = false; - int nLimit = vIPC.size(); - while (!fSuccess && nLimit-- > 0) - { - // Choose a random class C - unsigned int ipC = vIPC[GetRand(vIPC.size())]; - - // Organize all addresses in the class C by IP - map > mapIP; - CRITICAL_BLOCK(cs_mapAddresses) - { - unsigned int nDelay = ((30 * 60) << vNodes.size()); - if (nDelay > 8 * 60 * 60) - nDelay = 8 * 60 * 60; - for (map, CAddress>::iterator mi = mapAddresses.lower_bound(CAddress(ipC, 0).GetKey()); - mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey()); - ++mi) - { - const CAddress& addr = (*mi).second; - unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000; - if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000) - mapIP[addr.ip].push_back(addr); - } - } - if (mapIP.empty()) - break; - - // Choose a random IP in the class C - map >::iterator mi = mapIP.begin(); - advance(mi, GetRand(mapIP.size())); - - // Once we've chosen an IP, we'll try every given port before moving on - foreach(const CAddress& addrConnect, (*mi).second) - { - if (addrConnect.ip == addrLocalHost.ip || !addrConnect.IsIPv4() || FindNode(addrConnect.ip)) - continue; - - CNode* pnode = ConnectNode(addrConnect); - if (!pnode) - continue; - pnode->fNetworkNode = true; - - if (addrLocalHost.IsRoutable()) - { - // Advertise our address - vector vAddrToSend; - vAddrToSend.push_back(addrLocalHost); - pnode->PushMessage("addr", vAddrToSend); - } - - // Get as many addresses as we can - pnode->PushMessage("getaddr"); - - ////// should the one on the receiving end do this too? - // Subscribe our local subscription list - const unsigned int nHops = 0; - for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++) - if (pnodeLocalHost->vfSubscribe[nChannel]) - pnode->PushMessage("subscribe", nChannel, nHops); - - fSuccess = true; - break; - } - } - } -} @@ -1187,7 +935,7 @@ bool StartNode(string& strError) return false; } - // Get our external IP in a background thread — up to 4 HTTP requests with + // Get our external IP in a background thread -- up to 4 HTTP requests with // 5s timeouts each could otherwise delay startup by 20s on first run. // addrIncoming from a previous session is used immediately as a fallback. if (addrIncoming.ip) @@ -1228,13 +976,6 @@ bool StartNode(string& strError) return false; } - if (_beginthread(ThreadOpenConnections, 0, NULL) == -1) - { - strError = "Error: _beginthread(ThreadOpenConnections) failed"; - printf("%s\n", strError.c_str()); - return false; - } - if (_beginthread(ThreadMessageHandler, 0, NULL) == -1) { strError = "Error: _beginthread(ThreadMessageHandler) failed"; diff --git a/src/net.h b/src/net.h index e732eb3..f028c3b 100644 --- a/src/net.h +++ b/src/net.h @@ -22,12 +22,10 @@ enum -bool ConnectSocket(const CAddress& addrConnect, SOCKET& hSocketRet); bool GetMyExternalIP(unsigned int& ipRet); -bool AddAddress(CAddrDB& addrdb, const CAddress& addr); -CNode* FindNode(unsigned int ip); -CNode* ConnectNode(CAddress addrConnect, int64 nTimeout=0); CNode* ConnectNodeBtf(const string& strBtfAddr); +CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, + const unsigned char enc_pub[32]); void ThreadBtfAccept(void* parg); void ThreadBtfConnect(void* parg); extern string strBtfConnect; @@ -120,7 +118,7 @@ public: // Message size if (nMessageSize > 0x10000000) { - printf("CMessageHeader::IsValid() : nMessageSize too large %u\n", nMessageSize); + if (LogAcceptsCategory("net")) printf("CMessageHeader::IsValid() : nMessageSize too large %u\n", nMessageSize); return false; } @@ -294,7 +292,7 @@ public: void print() const { - printf("CAddress(%s)\n", ToString().c_str()); + if (LogAcceptsCategory("net")) printf("CAddress(%s)\n", ToString().c_str()); } }; @@ -387,7 +385,7 @@ public: void print() const { - printf("CInv(%s)\n", ToString().c_str()); + if (LogAcceptsCategory("net")) printf("CInv(%s)\n", ToString().c_str()); } }; @@ -426,13 +424,10 @@ extern bool fShutdown; extern array vfThreadRunning; extern vector vNodes; extern CCriticalSection cs_vNodes; -extern map, CAddress> mapAddresses; -extern CCriticalSection cs_mapAddresses; extern map mapRelay; extern deque > vRelayExpiration; extern CCriticalSection cs_mapRelay; extern map mapAlreadyAskedFor; -extern CAddress addrProxy; @@ -462,10 +457,6 @@ public: map mapRequests; CCriticalSection cs_mapRequests; - // flood - vector vAddrToSend; - set setAddrKnown; - // inventory based relay set setInventoryKnown; set setInventoryKnown2; @@ -555,7 +546,7 @@ public: // We're using mapAskFor as a priority queue, // the key is the earliest time the request can be sent int64& nRequestTime = mapAlreadyAskedFor[inv]; - printf("askfor %s %lld\n", inv.ToString().c_str(), nRequestTime); + if (LogAcceptsCategory("net")) printf("askfor %s %lld\n", inv.ToString().c_str(), nRequestTime); // Make sure not to reuse time indexes to keep things in the same order int64 nNow = (GetTime() - 1) * 1000000; @@ -576,7 +567,7 @@ public: AbortMessage(); nPushPos = vSend.size(); vSend << CMessageHeader(pszCommand, 0); - printf("sending: %-12s ", pszCommand); + if (LogAcceptsCategory("net")) printf("sending: %-12s ", pszCommand); } void AbortMessage() @@ -586,7 +577,7 @@ public: vSend.resize(nPushPos); nPushPos = -1; LeaveCriticalSection(&cs_vSend); - printf("(aborted)\n"); + if (LogAcceptsCategory("net")) printf("(aborted)\n"); } void EndMessage() @@ -594,7 +585,7 @@ public: extern int nDropMessagesTest; if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { - printf("dropmessages DROPPING SEND MESSAGE\n"); + if (LogAcceptsCategory("net")) printf("dropmessages DROPPING SEND MESSAGE\n"); AbortMessage(); return; } @@ -606,10 +597,10 @@ public: unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader); memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize)); - printf("(%d bytes) ", nSize); + if (LogAcceptsCategory("net")) printf("(%d bytes) ", nSize); //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++) // printf("%02x ", vSend[i] & 0xff); - printf("\n"); + if (LogAcceptsCategory("net")) printf("\n"); nPushPos = -1; LeaveCriticalSection(&cs_vSend); diff --git a/src/nostr.cpp b/src/nostr.cpp index c2ccc4c..009edd1 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -45,7 +45,12 @@ static const int64 BTF_POOL_MAX_AGE = 3 * 60; // seconds // Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous // before we stop dialing more, and how many new dials to attempt per cycle. -static const unsigned int BTF_TARGET_CONN = 8; +// Target outbound peer count. With ~100 known .btf peers on the network, +// an Erdos-Renyi random graph stays connected with overwhelming probability +// once average degree exceeds ln(N) (~4.6 here); 24 is ~5x past that +// threshold, so this is plenty *provided* selection isn't fully correlated +// across nodes -- see the diversity slice in ConnectDiscoveredBtfPeers. +static const unsigned int BTF_TARGET_CONN = 24; static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle // Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been @@ -58,11 +63,27 @@ static map g_btfPeers; static CCriticalSection cs_btfPeers; // Public discovery relays. Tunable. +// +// Picked for: free/public (no NIP-42 auth or payment wall), track record of +// high uptime, and *infrastructure* diversity, not just different hostnames. +// relay.damus.io, relay.snort.social and relay.primal.net are all fronted by +// Cloudflare -- different operators, same upstream, so a Cloudflare-side +// incident (or a Cloudflare IP range getting rate-limited/blocked on some +// network) can take out several of them at once even though they look like +// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the +// most established) and filled the rest with relays on different hosts +// (Hetzner, various independents) so no single upstream provider is a +// single point of failure for peer discovery. 7 relays lands inside the +// commonly recommended 5-10 range for redundancy without keeping an +// excessive number of persistent sockets open per node. const char* pszNostrRelays[] = { "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band", - "wss://relay.snort.social", + "wss://nostr21.com", + "wss://offchain.pub", + "wss://nostr.mom", + "wss://relay.primal.net", }; const int nNostrRelays = ARRAYLEN(pszNostrRelays); @@ -317,6 +338,13 @@ std::string BtfLocalAddress() return g_nostrKey.BtfAddress(); } +int GetDiscoveredPeerCount() +{ + int n = 0; + CRITICAL_BLOCK(cs_btfPeers) { n = (int)g_btfPeers.size(); } + return n; +} + void BtfGetPoolAnnouncements(std::vector& out) { out.clear(); @@ -425,38 +453,55 @@ public: size_t colon = host.find(':'); if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } - // Resolve and connect (TCP) - struct addrinfo hints, *res = NULL; + // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA + // records back and try them in the order the resolver prefers, + // rather than hard-failing whenever the IPv4 path to a relay is + // filtered/rate-limited (increasingly common for CDN-fronted + // relays) while a working IPv6 route exists right there in the + // same getaddrinfo() result. + struct addrinfo hints, *res = NULL, *rp = NULL; memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; + hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; char portstr[16]; sprintf(portstr, "%d", port); if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) return error("Nostr: getaddrinfo %s failed", host.c_str()); - hSocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (hSocket == INVALID_SOCKET) { freeaddrinfo(res); return false; } - // Socket receive/send timeout: Windows takes a DWORD of milliseconds; - // POSIX takes a struct timeval. + bool fConnected = false; + for (rp = res; rp != NULL; rp = rp->ai_next) + { + hSocket = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (hSocket == INVALID_SOCKET) + continue; + + // Socket receive/send timeout: Windows takes a DWORD of + // milliseconds; POSIX takes a struct timeval. #ifdef _WIN32 - DWORD tv = timeoutSec * 1000; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); + DWORD tv = timeoutSec * 1000; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); #else - struct timeval tv; - tv.tv_sec = timeoutSec; - tv.tv_usec = 0; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); + struct timeval tv; + tv.tv_sec = timeoutSec; + tv.tv_usec = 0; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); #endif - if (connect(hSocket, res->ai_addr, (int)res->ai_addrlen) != 0) - { - freeaddrinfo(res); - return error("Nostr: connect %s:%d failed", host.c_str(), port); + if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) + { + fConnected = true; + break; + } + + closesocket(hSocket); + hSocket = INVALID_SOCKET; } freeaddrinfo(res); + if (!fConnected) + return error("Nostr: connect %s:%d failed", host.c_str(), port); + // TLS if (fTls) { @@ -598,6 +643,88 @@ private: }; +// Persistent relay connections. Public relays (relay.damus.io in particular) +// rate-limit new connections per source IP fairly aggressively. Before this, +// every single operation -- self-test, per-pass seeding, batch descriptor +// resolve, pool announce, pool query -- opened its own fresh CWebSocket, +// meaning a single node could fire a dozen-plus brand-new TCP+TLS+WS +// handshakes at the same 4 relays within any given minute (ThreadNostrSeed's +// own loop plus the separate ThreadBtfPoolAnnouncer thread, each reconnecting +// from scratch every cycle). That's exactly what was showing up as constant +// "handshake refused by relay.damus.io" and "connect ... failed" -- not +// necessarily dead relays, just this node getting rate-limited by its own +// reconnect churn. +// +// Now every relay-touching call reuses one long-lived socket per relay via +// this pool, reconnecting only when a socket actually drops. That cuts fresh +// connections from several per minute down to essentially one per relay for +// the life of the process. +struct RelayConn +{ + CWebSocket ws; + CCriticalSection cs; // serializes use of this relay's socket + bool fConnected; + RelayConn() : fConnected(false) {} +}; +static map g_relayPool; +static CCriticalSection cs_relayPool; + +static RelayConn* GetRelayConn(const string& relay) +{ + CRITICAL_BLOCK(cs_relayPool) + { + map::iterator it = g_relayPool.find(relay); + if (it != g_relayPool.end()) + return it->second; + RelayConn* rc = new RelayConn(); + g_relayPool[relay] = rc; + return rc; + } + return NULL; // unreachable +} + +// RAII lease on a relay's pooled connection: connects (if not already +// connected) on construction, and on destruction either keeps the socket +// alive for the next caller (if the exchange succeeded) or closes it so the +// next caller reconnects instead of reusing a socket that may be wedged. +// Caller must set fOk = true once its exchange with the relay completed +// cleanly. +class CRelayLease +{ +public: + RelayConn* rc; + bool fOk; + bool fJustConnected; // true if this lease had to (re)connect, false if reusing a live socket + + CRelayLease(const string& relay, int timeoutSec) : rc(NULL), fOk(false), fJustConnected(false) + { + rc = GetRelayConn(relay); + rc->cs.Enter(); + if (!rc->fConnected) + { + rc->ws.Close(); + rc->fConnected = rc->ws.Connect(relay, timeoutSec); + fJustConnected = rc->fConnected; + } + } + ~CRelayLease() + { + if (!fOk) + { + rc->ws.Close(); + rc->fConnected = false; + } + rc->cs.Leave(); + } + bool Connected() const { return rc->fConnected; } + CWebSocket& Ws() { return rc->ws; } + +private: + CRelayLease(const CRelayLease&); + CRelayLease& operator=(const CRelayLease&); +}; + + // Publish this node's self-certifying `.btf` descriptor: "reach me (this key) // via , encrypting to my x25519 key ". Signed by the node // key so only the address's owner can publish it (no hijacking). @@ -673,11 +800,11 @@ bool BtfPublishPoolAnnouncement(const BtfPoolAnnouncement& ann) { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; - if (PublishPoolAnnouncementOnRelay(ws, g_nostrKey, ann)) - fPublished = true; + if (PublishPoolAnnouncementOnRelay(lease.Ws(), g_nostrKey, ann)) + fPublished = lease.fOk = true; } CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement") } @@ -699,9 +826,10 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; + CWebSocket& ws = lease.Ws(); json filter = json::object(); filter["authors"] = json::array({ HexEncode(pk, 32) }); @@ -756,6 +884,7 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen else if (type == "EOSE") break; } + lease.fOk = true; } CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement") } @@ -855,12 +984,16 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; btf::Descriptor d; - if (!ResolveDescriptor(ws, g_nostrKey.ctx, btfAddr, d)) + if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) + { + lease.fOk = true; // exchange completed, just no match continue; + } + lease.fOk = true; if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") continue; // descriptor predates the owner's relay config if (!HexDecode(d.enc, enc_pub, 32)) @@ -873,14 +1006,115 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, return false; } +// Resolve MANY .btf addresses in one pass. A single relay connection carries +// a REQ with every still-unresolved address's pubkey in "authors", instead of +// opening one relay connection per address. This matters because callers like +// ConnectDiscoveredBtfPeers dial many candidates in parallel; without +// batching, each of those dial threads independently walks the same handful +// of public relays on its own, so a pass of N candidates can fire up to +// N * nNostrRelays near-simultaneous connections at the same few relays -- +// enough to get rate-limited or have handshakes refused, which then looks +// like "dead peers" even though the descriptors are perfectly resolvable. +bool BtfResolveMany(const std::vector& btfAddrs, + std::map& out) +{ + if (!EnsureNostrKey() || btfAddrs.empty()) + return false; + + map hexToAddr; // pubkey hex -> .btf addr, for matching events back + set pending; + for (const string& a : btfAddrs) + { + unsigned char pk[32]; + if (!btf::ParseAddress(a, pk)) + continue; + hexToAddr[HexEncode(pk, 32)] = a; + pending.insert(a); + } + if (pending.empty()) + return false; + + for (int i = 0; i < nNostrRelays && !fShutdown && !pending.empty(); i++) + { + try + { + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) + continue; + CWebSocket& ws = lease.Ws(); + + json authors = json::array(); + for (auto& kv : hexToAddr) + if (pending.count(kv.second)) + authors.push_back(kv.first); + if (authors.empty()) + { + lease.fOk = true; + break; + } + + json filter = json::object(); + filter["authors"] = authors; + filter["kinds"] = json::array({ BTF_DESC_KIND }); + filter["limit"] = (int)authors.size(); + json req = json::array({ "REQ", "btf-resolve-many", filter }); + if (!ws.SendText(req.dump())) + continue; + + for (;;) + { + string msg; + if (!ws.RecvText(msg)) + break; + json j; + try { j = json::parse(msg); } catch (...) { continue; } + if (!j.is_array() || j.empty()) continue; + string t = j[0].get(); + if (t == "EVENT" && j.size() >= 3) + { + const json& ev = j[2]; + if (!ev.contains("pubkey") || !ev.contains("content")) + continue; + string hexpk = ev["pubkey"].get(); + auto ai = hexToAddr.find(hexpk); + if (ai == hexToAddr.end() || !pending.count(ai->second)) + continue; // not one we're waiting on (or already resolved) + + unsigned char pk[32]; + if (!HexDecode(hexpk, pk, 32)) + continue; + btf::Descriptor d; + if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get(), pk, d)) + continue; + if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") + continue; + BtfResolvedPeer rp; + if (!HexDecode(d.enc, rp.enc_pub, 32)) + continue; + rp.meetingHostPort = d.meeting_node; + out[ai->second] = rp; + pending.erase(ai->second); + } + else if (t == "EOSE") + break; + } + lease.fOk = true; + } + CATCH_PRINT_EXCEPTION("BtfResolveMany") + } + return !out.empty(); +} + // Connect to a relay: publish our .btf descriptor and collect relay data. static bool SeedFromRelay(CNostrKey& key, const string& relay) { - CWebSocket ws; - if (!ws.Connect(relay, 10)) + CRelayLease lease(relay, 10); + if (!lease.Connected()) return false; - printf("Nostr: connected to %s\n", relay.c_str()); + CWebSocket& ws = lease.Ws(); + if (lease.fJustConnected) + LogPrint("nostr", "Nostr: connected to %s\n", relay.c_str()); // Publish our self-certifying .btf descriptor (rendezvous discovery) PublishDescriptor(ws, key); @@ -930,7 +1164,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) else if (createdAt > it->second) it->second = createdAt; // refresh } if (fNew && fDebug) - printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str()); + LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); if (++nPeers > 500) break; } else if (type == "EOSE") @@ -974,7 +1208,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) fNew = g_discoveredRelays.insert(r).second; if (fNew && fDebug) - printf("Nostr: discovered relay %s\n", r.c_str()); + LogPrint("nostr", "Nostr: discovered relay %s\n", r.c_str()); } else if (type == "EOSE") break; @@ -1009,12 +1243,15 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) } } } + lease.fOk = true; return true; } +volatile bool gAnnounceNow = false; + void ThreadBtfPoolAnnouncer(void*) { - printf("ThreadBtfPoolAnnouncer started\n"); + LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); while (!fShutdown) { if (nMineMode == MINE_OPERATOR && gPoolRunning) @@ -1025,15 +1262,17 @@ void ThreadBtfPoolAnnouncer(void*) ann.dashboardUrl = strPoolDashboardUrl; ann.feePercent = dPoolFeePercent; uint64 roundShares = 0; - GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); - ann.hashRate = 0.0; + double totalHashRate = 0.0; + GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); + ann.hashRate = totalHashRate; ann.createdAt = GetTime(); if (!ann.btfAddress.empty() && !ann.poolName.empty()) BtfPublishPoolAnnouncement(ann); } - for (int i = 0; i < 60 && !fShutdown; i++) + for (int i = 0; i < 60 && !fShutdown && !gAnnounceNow; i++) Sleep(1000); + gAnnounceNow = false; } } @@ -1042,10 +1281,29 @@ void ThreadBtfPoolAnnouncer(void*) // dead/stale descriptors (e.g. abandoned test identities that linger on the // relays) from consuming the whole per-pass dial budget and starving the live // peers. Backoff grows on repeated failures, capped. +// +// g_btfPeerFails also doubles as the priority signal for dial ordering: a peer +// we've connected to before (present in g_btfPeerEverConnected) is known-good +// and goes first; a peer we've never tried goes next; a peer with a growing +// fail count is known-bad and goes last, worst offenders furthest back. This +// replaces the old behavior where the whole candidate list was shuffled right +// after being sorted by freshness, which made the freshness sort a no-op and +// gave dead peers equal footing with everyone else. static map g_btfPeerBackoff; // addr -> next-try time static map g_btfPeerFails; // addr -> consecutive failures +static set g_btfPeerEverConnected; // addr -> has succeeded at least once static CCriticalSection cs_btfPeerState; +// Backoff cap for repeatedly-dead peers. The old 15-minute cap meant a +// descriptor that has failed many times in a row cycles back into the dial +// pool just as often as a peer we've never tried, wasting dial budget on +// known-dead addresses every pass. Once a peer has failed enough times to +// be "chronic", stretch the cap much further so it consumes budget far less +// often while still getting retried eventually in case it comes back online. +static const int64 BTF_BACKOFF_CAP_SECS = 15 * 60; // 15 min: initial ramp cap +static const int64 BTF_BACKOFF_CAP_SECS_DEAD = 6 * 60 * 60; // 6 hr: cap for chronic failures +static const int BTF_CHRONIC_FAIL_THRESHOLD = 6; // fails before "chronic" cap kicks in + // Dial the .btf peers discovered on the relays, up to a target connection count. // Connections are attempted in parallel so a single slow/dead relay doesn't // stall the entire pass. Each candidate gets its own thread; all threads start @@ -1060,23 +1318,18 @@ static void ConnectDiscoveredBtfPeers() for (auto& kv : g_btfPeers) peersWithAge.push_back({kv.second, kv.first}); } - // Sort newest descriptor first — recently-active nodes are much more - // likely to be online right now. Stale ones stay in the list as fallback. - sort(peersWithAge.begin(), peersWithAge.end(), - [](const pair& a, const pair& b){ return a.first > b.first; }); - int64 cutoff = GetTime() - BTF_PEER_MAX_AGE; int nFresh = 0; - vector peers; + vector> fresh; // (createdAt, addr), newest first for (auto& p : peersWithAge) { if (p.first < cutoff) continue; - peers.push_back(p.second); + fresh.push_back(p); nFresh++; } - if (!peers.empty()) - printf("Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); + if (!fresh.empty()) + LogPrint("nostr", "Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); // Drop stale peers so we stop retrying dead descriptors until they refresh. { @@ -1092,42 +1345,164 @@ static void ConnectDiscoveredBtfPeers() } } - // Shuffle so we don't always retry the same dead peers at the front. - for (size_t i = peers.size(); i > 1; i--) - swap(peers[i - 1], peers[(size_t)GetRand(i)]); + // Sort newest descriptor first -- recently-active nodes are much more + // likely to be online right now. + sort(fresh.begin(), fresh.end(), + [](const pair& a, const pair& b){ return a.first > b.first; }); + + // Reorder into tiers: known-good (succeeded before) first, untried next, + // known-bad (has a fail count) last -- worst offenders furthest back. + // Freshness order is preserved *within* each tier, so recency still + // matters, but a peer we know is dead no longer gets equal footing with + // a peer we've never tried or one that's worked before. + vector tierGood, tierUntried, tierBad; + CRITICAL_BLOCK(cs_btfPeerState) + { + for (auto& p : fresh) + { + const string& addr = p.second; + if (g_btfPeerEverConnected.count(addr)) + tierGood.push_back(addr); + else if (g_btfPeerFails.find(addr) == g_btfPeerFails.end()) + tierUntried.push_back(addr); + else + tierBad.push_back(addr); + } + // Within known-bad, put fewer-fails peers ahead of chronic offenders. + sort(tierBad.begin(), tierBad.end(), + [](const string& a, const string& b){ return g_btfPeerFails[a] < g_btfPeerFails[b]; }); + } + + vector peers; + peers.reserve(fresh.size()); + peers.insert(peers.end(), tierGood.begin(), tierGood.end()); + peers.insert(peers.end(), tierUntried.begin(), tierUntried.end()); + peers.insert(peers.end(), tierBad.begin(), tierBad.end()); // Pick candidates for this pass (skip those in backoff, cap at budget). + // + // The tiered order above is deliberately biased toward peers we know + // are good -- that's the right default, since dialing untested/bad + // peers wastes attempts. But if every node on the network applies the + // same bias, everyone converges on the same small set of "known good" + // peers as neighbors: that correlates peer sets across the network, + // which is exactly what standard random-graph connectivity results + // assume you avoid. If that shared hub set has a bad day (e.g. one + // relay outage), many nodes lose the same edges at once instead of + // losing independent, uncorrelated edges. + // + // So: fill most of the pass from the reliable/tiered order, but reserve + // a slice for peers picked at random from the *entire* fresh pool + // (not just the front of the sorted list). Same pattern Bitcoin's own + // peer manager and Kademlia-style DHTs use -- sticky reliable peers + // plus a random sample, not one or the other. int64 now = GetTime(); + int nBudget = (int)BTF_DIALS_PER_PASS; + { + int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size(); + if (nRoom < nBudget) nBudget = nRoom; + } + if (nBudget < 0) nBudget = 0; + int nRandomSlots = (nBudget > 0) ? max(1, nBudget * 3 / 10) : 0; + int nSortedSlots = nBudget - nRandomSlots; + vector candidates; + set setChosen; CRITICAL_BLOCK(cs_btfPeerState) { + // Reliability slice: walk the tiered (good -> untried -> bad) order. for (const string& addr : peers) { if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break; - if ((int)candidates.size() >= BTF_DIALS_PER_PASS) break; + if ((int)candidates.size() >= nSortedSlots) break; map::iterator bi = g_btfPeerBackoff.find(addr); if (bi != g_btfPeerBackoff.end() && now < bi->second) continue; candidates.push_back(addr); + setChosen.insert(addr); + } + + // Diversity slice: random sample from the full fresh pool, so the + // dial isn't limited to whatever the tiered sort put up front. + vector pool; + for (auto& p : fresh) + { + const string& addr = p.second; + if (setChosen.count(addr)) + continue; + map::iterator bi = g_btfPeerBackoff.find(addr); + if (bi != g_btfPeerBackoff.end() && now < bi->second) + continue; + pool.push_back(addr); + } + int nWant = min(nRandomSlots, (int)pool.size()); + for (int i = 0; i < nWant; i++) + { + if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; + if ((int)candidates.size() >= nBudget) break; + size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); + std::swap(pool[i], pool[j]); + candidates.push_back(pool[i]); + setChosen.insert(pool[i]); } } if (candidates.empty()) return; - // Launch one thread per candidate — all connect attempts run in parallel. + // Resolve every candidate's descriptor in one batched pass (a handful of + // relay round-trips total, one REQ per relay covering all candidates) + // instead of each dial thread independently walking the relay list on + // its own. That per-thread approach meant a 12-candidate pass could fire + // up to 12 * nNostrRelays near-simultaneous connections at the same + // handful of public relays -- enough to trip rate limits or get + // handshakes refused, which then looks exactly like "dead peers" even + // though the descriptors were perfectly resolvable. + map resolved; + BtfResolveMany(candidates, resolved); + + // Candidates whose descriptor didn't resolve this pass are recorded as a + // failure right away (no point spawning a tunnel thread with nothing to + // dial), same bookkeeping as a failed connect attempt. + vector toDial; + CRITICAL_BLOCK(cs_btfPeerState) + { + for (const string& addr : candidates) + { + if (resolved.count(addr)) + { + toDial.push_back(addr); + continue; + } + int n = ++g_btfPeerFails[addr]; + int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD + : BTF_BACKOFF_CAP_SECS; + int64 secs = 30; + for (int k = 1; k < n && secs < cap; k++) secs *= 2; + if (secs > cap) secs = cap; + g_btfPeerBackoff[addr] = GetTime() + secs; + } + } + if (toDial.empty()) return; + + // Launch one thread per resolved candidate -- all connect attempts run in + // parallel, but purely against each peer's own rendezvous meeting node, + // not the shared Nostr relays, so there's no thundering-herd effect here. // Each thread writes its result back via shared state guarded by cs_btfPeerState. - struct DialCtx { string addr; }; + struct DialCtx { string addr; BtfResolvedPeer rp; }; vector ctxs; - for (const string& addr : candidates) + for (const string& addr : toDial) { - DialCtx* ctx = new DialCtx{addr}; + DialCtx* ctx = new DialCtx{addr, resolved[addr]}; ctxs.push_back(ctx); _beginthread([](void* arg) { DialCtx* ctx = (DialCtx*)arg; string addr = ctx->addr; + string meeting = ctx->rp.meetingHostPort; + unsigned char enc_pub[32]; + memcpy(enc_pub, ctx->rp.enc_pub, 32); delete ctx; - CNode* pnode = ConnectNodeBtf(addr); + CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); CRITICAL_BLOCK(cs_btfPeerState) { if (pnode) @@ -1138,13 +1513,16 @@ static void ConnectDiscoveredBtfPeers() pnode->Release(); g_btfPeerBackoff.erase(addr); g_btfPeerFails.erase(addr); + g_btfPeerEverConnected.insert(addr); } else { int n = ++g_btfPeerFails[addr]; + int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD + : BTF_BACKOFF_CAP_SECS; int64 secs = 30; - for (int k = 1; k < n && secs < 15*60; k++) secs *= 2; - if (secs > 15*60) secs = 15*60; + for (int k = 1; k < n && secs < cap; k++) secs *= 2; + if (secs > cap) secs = cap; g_btfPeerBackoff[addr] = GetTime() + secs; } } @@ -1160,16 +1538,16 @@ static void ConnectDiscoveredBtfPeers() void ThreadNostrSeed(void* parg) { - printf("ThreadNostrSeed started\n"); + LogPrint("nostr", "ThreadNostrSeed started\n"); if (!EnsureNostrKey()) { - printf("Nostr: failed to initialize secp256k1 key\n"); + LogPrint("nostr", "Nostr: failed to initialize secp256k1 key\n"); return; } CNostrKey& key = g_nostrKey; - printf("Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); - printf("Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); + LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); + LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); // One-time self-test of the rendezvous: publish our descriptor to a relay, // then resolve our OWN .btf address back and verify it self-certifies. Proves @@ -1178,17 +1556,20 @@ void ThreadNostrSeed(void* parg) { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) continue; + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; + CWebSocket& ws = lease.Ws(); PublishDescriptor(ws, key); Sleep(700); // let the relay store the replaceable event btf::Descriptor d; if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) { - printf("Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", + LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); + lease.fOk = true; break; } + lease.fOk = true; } CATCH_PRINT_EXCEPTION("btf self-test") } @@ -1223,8 +1604,9 @@ void ThreadNostrSeed(void* parg) ann.dashboardUrl = strPoolDashboardUrl; ann.feePercent = dPoolFeePercent; uint64 roundShares = 0; - GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); - ann.hashRate = 0.0; + double totalHashRate = 0.0; + GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); + ann.hashRate = totalHashRate; ann.createdAt = GetTime(); if (!ann.btfAddress.empty() && !ann.poolName.empty()) BtfPublishPoolAnnouncement(ann); diff --git a/src/nostr.h b/src/nostr.h index 63fc095..37b3b05 100644 --- a/src/nostr.h +++ b/src/nostr.h @@ -12,11 +12,17 @@ #include #include +#include // Thread entry point (signature compatible with _beginthread) void ThreadNostrSeed(void* parg); void ThreadBtfPoolAnnouncer(void* parg); +// Set to true to make ThreadBtfPoolAnnouncer publish immediately instead of +// waiting for its ~60s sleep loop (e.g. right after the operator changes pool +// name/fee/dashboard URL in Options). The announcer clears it after publishing. +extern volatile bool gAnnounceNow; + // Public relays used for discovery. Tunable. extern const char* pszNostrRelays[]; extern const int nNostrRelays; @@ -56,6 +62,9 @@ struct BtfPoolAnnouncement } }; +// Number of .btf peers discovered from Nostr relays (includes not-yet-connected). +int GetDiscoveredPeerCount(); + // Current live pool announcements discovered from Nostr relays. void BtfGetPoolAnnouncements(std::vector& out); @@ -78,4 +87,21 @@ std::string BtfLocalAddress(); bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, unsigned char enc_pub[32]); +// Result of resolving one .btf address via BtfResolveMany. +struct BtfResolvedPeer +{ + std::string meetingHostPort; + unsigned char enc_pub[32]; +}; + +// Resolve many .btf addresses at once. Batches all still-unresolved addresses +// into a single Nostr REQ per relay (multiple "authors"), instead of opening +// one relay connection per address like BtfResolve does. Use this whenever +// resolving more than a handful of addresses in the same pass -- e.g. before +// dialing several discovered peers in parallel -- to avoid hammering the +// same few public relays with a burst of simultaneous connections. Returns +// true if at least one address resolved; check `out` for which ones. +bool BtfResolveMany(const std::vector& btfAddrs, + std::map& out); + #endif diff --git a/src/rc/addressbook16.bmp b/src/rc/addressbook16.bmp deleted file mode 100644 index c557691..0000000 Binary files a/src/rc/addressbook16.bmp and /dev/null differ diff --git a/src/rc/addressbook16mask.bmp b/src/rc/addressbook16mask.bmp deleted file mode 100644 index d3a478d..0000000 Binary files a/src/rc/addressbook16mask.bmp and /dev/null differ diff --git a/src/rc/addressbook20.bmp b/src/rc/addressbook20.bmp deleted file mode 100644 index 2b33b22..0000000 Binary files a/src/rc/addressbook20.bmp and /dev/null differ diff --git a/src/rc/addressbook20mask.bmp b/src/rc/addressbook20mask.bmp deleted file mode 100644 index 56ce612..0000000 Binary files a/src/rc/addressbook20mask.bmp and /dev/null differ diff --git a/src/rc/bitcoin_bc_original.ico b/src/rc/bitcoin_bc_original.ico deleted file mode 100644 index 88cc240..0000000 Binary files a/src/rc/bitcoin_bc_original.ico and /dev/null differ diff --git a/src/rc/check.ico b/src/rc/check.ico deleted file mode 100644 index 0c4e6e8..0000000 Binary files a/src/rc/check.ico and /dev/null differ diff --git a/src/rc/send16.bmp b/src/rc/send16.bmp deleted file mode 100644 index 676b5c4..0000000 Binary files a/src/rc/send16.bmp and /dev/null differ diff --git a/src/rc/send16mask.bmp b/src/rc/send16mask.bmp deleted file mode 100644 index 06c747f..0000000 Binary files a/src/rc/send16mask.bmp and /dev/null differ diff --git a/src/rc/send16masknoshadow.bmp b/src/rc/send16masknoshadow.bmp deleted file mode 100644 index faf24e0..0000000 Binary files a/src/rc/send16masknoshadow.bmp and /dev/null differ diff --git a/src/rc/send20.bmp b/src/rc/send20.bmp deleted file mode 100644 index 2b90422..0000000 Binary files a/src/rc/send20.bmp and /dev/null differ diff --git a/src/rc/send20mask.bmp b/src/rc/send20mask.bmp deleted file mode 100644 index f124d0d..0000000 Binary files a/src/rc/send20mask.bmp and /dev/null differ diff --git a/src/rpc.cpp b/src/rpc.cpp index 99d182e..1b529e8 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1,14 +1,19 @@ // Copyright (c) 2009 Satoshi Nakamoto / Copyright (c) 2026 Bitflash developers // Distributed under the MIT/X11 software license. // -// Bitflash pool server — .btf-only pool listener. -// Miners connect through rendezvous tunnels; no raw IP:port endpoints remain. -// Pending payouts are held until maturity and shown in the GUI. +// Bitflash pool server -- .btf Stratum pool, operator side. // -// Compiles on both Windows (mingw) and Linux unchanged. -// Launched from ui.cpp (GUI) and headless.cpp (node) via ThreadRPCServer(). +// Architecture: +// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout +// state. No locks needed on any of that data. +// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister + +// BtfServiceWrap, pushes finished socket to gReadyQueue. +// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every +// 60s so dynamically discovered relays get picked up. +// +// GUI-visible state is snapshotted under gStatsMutex every 2s. +// Accept queue handoff uses gReadyMutex (held for microseconds). -// nlohmann must be included before headers.h because util.h redefines snprintf #pragma push_macro("snprintf") #undef snprintf #include @@ -20,8 +25,6 @@ #endif #include "btftunnel.h" -// POSIX socket headers — already provided by compat.h on Windows, -// and by the OS directly on Linux. #ifndef _WIN32 #include #include @@ -33,14 +36,15 @@ #include #include #include +#include #include +#include +#include #include #include -#include using json = nlohmann::json; -// Cross-platform socket close / send flags #ifdef _WIN32 #define sock_close(s) closesocket(s) #define SEND_FLAGS 0 @@ -49,660 +53,1260 @@ using json = nlohmann::json; #define SEND_FLAGS MSG_NOSIGNAL #endif -// Cross-platform thread launch: wraps _beginthread (compat.h maps it to -// pthread_create+detach on Linux, native _beginthread on Windows). -// fn must be void(*)(void*). -static void LaunchThread(void (*fn)(void*), void* arg) -{ - _beginthread(fn, 0, arg); -} - -// --------------------------------------------------------------------------- -// Config — set by headless.cpp / ui.cpp before threads launch -// --------------------------------------------------------------------------- -std::string gRpcUser; -std::string gRpcPassword; - -// Pool server running flag — set false to stop server cleanly (all loops check it) volatile bool gPoolRunning = false; -// --------------------------------------------------------------------------- -// Stubs for symbols absent from Bitflash 0.1.0 -// --------------------------------------------------------------------------- -double dHashesPerSec = 0.0; -int64 nHPSTimerStart = 0; - -static bool IsInitialBlockDownload() -{ - return (pindexBest == NULL || nBestHeight < 1); -} - // --------------------------------------------------------------------------- // Hex helpers // --------------------------------------------------------------------------- static std::string ToHex(const void* p, size_t n) { const unsigned char* b = (const unsigned char*)p; - std::string s; s.reserve(n*2); + std::string s; s.reserve(n * 2); static const char* H = "0123456789abcdef"; - for (size_t i = 0; i < n; i++) { s += H[b[i]>>4]; s += H[b[i]&0xf]; } + for (size_t i = 0; i < n; i++) { s += H[b[i] >> 4]; s += H[b[i] & 0xf]; } return s; } + static std::vector FromHex(const std::string& s) { std::vector v; - for (size_t i = 0; i+1 < s.size(); i += 2) { - auto h=[](char c)->int{ - if(c>='0'&&c<='9') return c-'0'; - if(c>='a'&&c<='f') return c-'a'+10; - if(c>='A'&&c<='F') return c-'A'+10; + for (size_t i = 0; i + 1 < s.size(); i += 2) { + auto h = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; return 0; }; - v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1]))); + v.push_back((unsigned char)((h(s[i]) << 4) | h(s[i + 1]))); } return v; } // --------------------------------------------------------------------------- -// PPLNS share tracking -// --------------------------------------------------------------------------- -struct Share { - std::string minerAddress; - int64 timestamp; - uint64 difficulty; -}; - -struct MinerStats { - std::string address; - std::string worker; - uint64 totalShares = 0; - uint64 roundShares = 0; - int64 lastSeen = 0; -}; -static std::mutex gMinerStatsMutex; -static std::map gMinerStats; - -struct FoundBlock { - int height; - int64 timestamp; - std::string minerAddress; - uint256 hash; -}; -static std::mutex gBlocksMutex; -static std::deque gFoundBlocks; - -struct Miner; -static std::mutex gMinersMutex; -static std::vector gMiners; - -static std::string MinerKey(const std::string& address, const std::string& worker) -{ - return address + "\n" + worker; -} - -void GetPoolWorkerStats(std::vector& out) -{ - out.clear(); - int64 now = GetTime(); - std::lock_guard lk(gMinerStatsMutex); - for (std::map::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - { - const MinerStats& ms = it->second; - if (ms.lastSeen <= 0) - continue; - if (now - ms.lastSeen > 15 * 60) - continue; - PoolWorkerStatView view; - view.address = ms.address; - view.worker = ms.worker; - view.totalShares = ms.totalShares; - view.roundShares = ms.roundShares; - view.lastSeen = ms.lastSeen; - out.push_back(view); - } - std::sort(out.begin(), out.end(), [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { - if (a.roundShares != b.roundShares) return a.roundShares > b.roundShares; - if (a.totalShares != b.totalShares) return a.totalShares > b.totalShares; - return a.lastSeen > b.lastSeen; - }); -} - -// --------------------------------------------------------------------------- -// Current mining job +// Job -- written and read exclusively by the event loop thread // --------------------------------------------------------------------------- struct StratumJob { std::string jobId; CBlock block; - uint256 target; - uint256 shareTarget; + uint256 target; // full block target; share targets are per-miner via vardiff int height; }; +static StratumJob gCurrentJob; +static bool gHaveJob = false; + +// Compute a share target from a Stratum difficulty value. +// Stratum difficulty 1 must correspond to THIS chain's easiest allowed target +// (bnProofOfWorkLimit = ~uint256(0) >> 12, compact 0x1f0fffff), not Bitcoin's +// SHA256 diff-1 target (0x1d00ffff). Those differ by about 2^20 (~1e6x): +// Bitcoin's diff-1 needs ~2^32 hashes/share on average, while this chain's PoW +// floor needs only ~2^12. Using the Bitcoin constant made every miner's +// "difficulty 1" about a million times harder than the chain's own floor, so a +// worker could hash for minutes without ever finding a share -- and vardiff +// never gets the samples it needs to correct course, because no share ever +// comes in for it to measure. Base off the actual chain limit instead so +// difficulty 1 is genuinely easy and vardiff has real data to retarget from. +// Higher difficulty = smaller (harder) target. +static uint256 ShareTargetFromDifficulty(double difficulty) +{ + if (difficulty <= 0.0) difficulty = 1.0; + static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); + // shareTarget = diff1 / difficulty + CBigNum bn; bn.setuint256(diff1); + // Multiply by 2^16 to keep precision, divide, shift back + bn <<= 16; + bn /= (uint64)(difficulty * 65536.0); + uint256 t = bn.getuint256(); + // Cap at all-ones (maximum, easiest possible) + if (t < diff1 && difficulty < 1.0) t = ~uint256(0); + return t; +} -static std::mutex gJobMutex; -static StratumJob gCurrentJob; -static bool gHaveJob = false; -static std::atomic gJobSeq{0}; - -static uint256 MakeShareTarget(const uint256& blockTarget) +// Expected number of hashes to find one share at a given Stratum difficulty, +// on this chain's scale (difficulty 1 == bnProofOfWorkLimit, which needs on +// average 2^12 = 4096 hashes). Used to turn "shares per second" into H/s. +static const double SHARE_HASHES_AT_DIFF_1 = 4096.0; +static double ExpectedHashesForDifficulty(double difficulty) { - uint256 shareTarget = blockTarget; - shareTarget <<= 8; - if (shareTarget < blockTarget) - shareTarget = ~uint256(0); - return shareTarget; + if (difficulty <= 0.0) difficulty = 1.0; + return difficulty * SHARE_HASHES_AT_DIFF_1; } +// Rebuild the current job from the best chain tip. +// Must be called from the event loop thread (or startup, before the loop). +// Returns true on success. Logs the specific failure reason on false. static bool RebuildJob() { - if (!pindexBest) return false; + CRITICAL_BLOCK(cs_main) + { + if (!pindexBest) { + LogPrint("pool", "RebuildJob: no chain tip (pindexBest is null)\n"); + return false; + } - CBlockIndex* pindexPrev = pindexBest; - unsigned int nBits = GetNextWorkRequired(pindexPrev); + CBlockIndex* pindexPrev = pindexBest; + unsigned int nBits = GetNextWorkRequired(pindexPrev); - CKey key; - key.MakeNewKey(); - static std::atomic sExtra{0}; - uint32_t extraNonce = ++sExtra; + CKey key; key.MakeNewKey(); + static std::atomic sExtra{0}; + uint32_t extraNonce = ++sExtra; - CTransaction txNew; - txNew.vin.resize(1); - txNew.vin[0].prevout.SetNull(); - txNew.vin[0].scriptSig << nBits << (CBigNum)extraNonce; - txNew.vout.resize(1); - // Always pay coinbase to a fresh wallet key. DoPayouts() distributes - // shares to miners via SendMoney() after the block is accepted. - txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; + CTransaction txNew; + txNew.vin.resize(1); + txNew.vin[0].prevout.SetNull(); + txNew.vin[0].scriptSig << nBits << (CBigNum)extraNonce; + txNew.vout.resize(1); + txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; - CBlock block; - block.vtx.push_back(txNew); + CBlock block; + block.vtx.push_back(txNew); - { - CTxDB txdb("r"); - CRITICAL_BLOCK(cs_mapTransactions) { - map pool; - unsigned int sz = 0; - for (auto& kv : mapTransactions) { - CTransaction& tx = kv.second; - if (tx.IsCoinBase() || !tx.IsFinal()) continue; - int64 nFees = 0; - int64 nMinFee = tx.GetMinFee(block.vtx.size() < 100); - map tmp(pool); - if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, false, true, nMinFee)) continue; - pool = tmp; - block.vtx.push_back(tx); - sz += ::GetSerializeSize(tx, SER_NETWORK); - if (sz > MAX_SIZE/2) break; + CTxDB txdb("r"); + CRITICAL_BLOCK(cs_mapTransactions) + { + map pool; + unsigned int sz = 0; + for (auto& kv : mapTransactions) { + CTransaction& tx = kv.second; + if (tx.IsCoinBase() || !tx.IsFinal()) continue; + int64 nFees = 0; + map tmp(pool); + if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, + false, true, tx.GetMinFee(block.vtx.size() < 100))) + continue; + pool = tmp; + block.vtx.push_back(tx); + sz += ::GetSerializeSize(tx, SER_NETWORK); + if (sz > MAX_SIZE / 2) break; + } } } + + block.vtx[0].vout[0].nValue = block.GetBlockValue(0); + block.hashPrevBlock = pindexPrev->GetBlockHash(); + block.hashMerkleRoot = block.BuildMerkleTree(); + block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), + (unsigned int)GetAdjustedTime()); + block.nBits = nBits; + block.nNonce = 1; + AddKey(key); + + StratumJob job; + job.jobId = ToHex(&extraNonce, 4); + job.block = block; + job.target = CBigNum().SetCompact(nBits).getuint256(); + job.height = nBestHeight + 1; + + gCurrentJob = job; + gHaveJob = true; + return true; } - block.vtx[0].vout[0].nValue = block.GetBlockValue(0); - block.hashPrevBlock = pindexPrev->GetBlockHash(); - block.hashMerkleRoot = block.BuildMerkleTree(); - block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast()+1), - (unsigned int)GetAdjustedTime()); - block.nBits = nBits; - block.nNonce = 1; - - AddKey(key); - - StratumJob job; - job.jobId = ToHex(&extraNonce, 4); - job.block = block; - job.target = CBigNum().SetCompact(nBits).getuint256(); - job.shareTarget = MakeShareTarget(job.target); - job.height = nBestHeight + 1; - - std::lock_guard lk(gJobMutex); - gCurrentJob = job; - gHaveJob = true; - ++gJobSeq; - return true; + return false; } // --------------------------------------------------------------------------- -// PPLNS payout -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// Deferred payouts — coinbase outputs require COINBASE_MATURITY (100) blocks -// before they can be spent. We store the share snapshot when a block is found -// and execute the actual SendMoney() calls once the coinbase matures. +// Payouts -- event loop thread only // --------------------------------------------------------------------------- struct PendingPayout { - int matureAtHeight; // pay out when nBestHeight >= this - std::map amounts; // addr -> BTF amount + int matureAtHeight; + std::map amounts; // address -> satoshis }; +static std::vector gPendingPayouts; -static std::mutex gPayoutMutex; -static std::vector gPendingPayouts; +static std::string PayoutFilePath() +{ + return GetAppDir() + "/pending_payouts.json"; +} -void GetPendingPayouts(std::vector& out) +static void SavePendingPayouts() { - out.clear(); - std::lock_guard lk(gPayoutMutex); - for (const PendingPayout& p : gPendingPayouts) - { - PendingPayoutView view; - view.matureAtHeight = p.matureAtHeight; - view.recipients = (int)p.amounts.size(); - view.totalAmount = 0; - for (const std::map::value_type& kv : p.amounts) - view.totalAmount += kv.second; - out.push_back(view); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& pp : gPendingPayouts) { + nlohmann::json obj; + obj["matureAtHeight"] = pp.matureAtHeight; + nlohmann::json amounts = nlohmann::json::object(); + for (const auto& kv : pp.amounts) + amounts[kv.first] = kv.second; + obj["amounts"] = amounts; + arr.push_back(obj); + } + std::string path = PayoutFilePath(); + FILE* f = fopen(path.c_str(), "w"); + if (f) { + std::string s = arr.dump(2); + fwrite(s.c_str(), 1, s.size(), f); + fclose(f); + } else { + LogPrint("payout", "[payout] WARNING: could not write %s\n", path.c_str()); } } -// Called when a block is found: snapshot shares, schedule payout for maturity. -static void QueuePayouts(int blockHeight) +static void LoadPendingPayouts() { - if (nMineMode != MINE_OPERATOR) return; - - std::map shareCount; - uint64 total = 0; - { - std::lock_guard lk(gMinerStatsMutex); - for (std::map::iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - { - MinerStats& ms = it->second; - if (ms.roundShares == 0) - continue; - if (ms.address.empty()) - continue; - shareCount[ms.address] += ms.roundShares; - total += ms.roundShares; - ms.roundShares = 0; + std::string path = PayoutFilePath(); + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; // no file yet -- first run, nothing to load + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + std::string s(sz, '\0'); + fread(&s[0], 1, sz, f); + fclose(f); + try { + nlohmann::json arr = nlohmann::json::parse(s); + for (const auto& obj : arr) { + PendingPayout pp; + pp.matureAtHeight = obj["matureAtHeight"].get(); + for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it) + pp.amounts[it.key()] = it.value().get(); + gPendingPayouts.push_back(pp); } + LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n", + gPendingPayouts.size()); + } catch (const std::exception& e) { + LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", + path.c_str(), e.what()); + } +} + +// Payouts ready to execute are moved here under gPayoutExecMutex. +// A dedicated thread drains this list so SendMoney (which takes cs_main) +// never runs on the event loop thread and can't stall miner I/O. +static std::vector gPayoutExecQueue; +static std::mutex gPayoutExecMutex; +static std::atomic gPayoutThreadRunning{false}; + +static void QueuePayouts(int blockHeight, + const std::map& shareCount, + uint64 total) +{ + if (total == 0) { + LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", + blockHeight); + return; } - if (total == 0) return; - int64 blockReward = CBlock().GetBlockValue(0); + double feeFrac = dPoolFeePercent / 100.0; + if (feeFrac < 0.0) feeFrac = 0.0; + if (feeFrac > 1.0) feeFrac = 1.0; + + // Compute the block subsidy at the height that was just found. + // We do not include tx fees in miner payout -- those stay in the operator + // wallet as part of the coinbase, which is the operator's cut on top of feeFrac. + int64 nSubsidy = 50 * COIN; + nSubsidy >>= (blockHeight / 210000); // halving schedule + int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); + PendingPayout pp; pp.matureAtHeight = blockHeight + COINBASE_MATURITY; + + int skipped = 0; + int64 burned = 0; // amount owed to miners with bad/empty addresses for (auto& kv : shareCount) { - int64 amount = (int64)(((double)kv.second / (double)total) * (double)blockReward); - if (amount >= CENT) - pp.amounts[kv.first] = amount; + int64 amount = (int64)(((double)kv.second / (double)total) * (double)minerReward); + if (amount < CENT) { + skipped++; + continue; + } + uint160 addrCheck; + if (kv.first.empty() || !AddressToHash160(kv.first, addrCheck)) { + // Address was invalid at authorize time (warned then). Their share + // of the reward cannot be paid -- log the amount so it's visible. + burned += amount; + LogPrint("payout", "[payout] WARNING: %s owed to miner with invalid" + " address '%s' -- cannot pay out, amount forfeited\n", + FormatMoney(amount).c_str(), kv.first.c_str()); + continue; + } + pp.amounts[kv.first] = amount; } - { std::lock_guard lk(gPayoutMutex); gPendingPayouts.push_back(pp); } - - printf("Pool: block %d payout queued for height %d (%zu miners)\n", - blockHeight, pp.matureAtHeight, pp.amounts.size()); + gPendingPayouts.push_back(pp); + SavePendingPayouts(); + + int64 operatorCut = nSubsidy - minerReward; + LogPrint("payout", + "[payout] block %d: subsidy %s BTF, miners get %s" + " (fee %.2f%% = %s to operator)," + " %zu recipients, matures height %d," + " %d below dust%s\n", + blockHeight, + FormatMoney(nSubsidy).c_str(), + FormatMoney(minerReward).c_str(), + dPoolFeePercent, + FormatMoney(operatorCut).c_str(), + pp.amounts.size(), pp.matureAtHeight, skipped, + burned > 0 ? (", " + FormatMoney(burned) + " forfeited (bad address)").c_str() + : ""); } -// Called from JobWatcher every new block: execute any matured payouts. +// Called from event loop: move matured entries to the exec queue (fast). +// Save BEFORE moving to exec queue: if we crash between the save and SendMoney, +// LoadPendingPayouts re-queues them from the exec-queue section of the file. static void FlushMaturePayouts() { - if (nMineMode != MINE_OPERATOR) return; - - std::lock_guard lk(gPayoutMutex); + std::vector ready; for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { if (nBestHeight < it->matureAtHeight) { ++it; continue; } - - printf("Pool: executing payout for block matured at height %d\n", it->matureAtHeight); - for (auto& kv : it->amounts) { - uint160 hash160; - if (!AddressToHash160(kv.first, hash160)) continue; - CScript scriptPubKey; - scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; - CWalletTx wtx; - wtx.mapValue["comment"] = strprintf("Pool payout height %d", it->matureAtHeight); - if (SendMoney(scriptPubKey, kv.second, wtx)) - printf("Pool: paid %s to %s\n", FormatMoney(kv.second).c_str(), kv.first.c_str()); + LogPrint("payout", "[payout] block height %d matured -- queuing %zu payments\n", + it->matureAtHeight, it->amounts.size()); + ready.push_back(*it); // copy first; erase after save + ++it; + } + if (!ready.empty()) { + // Erase from pending then save before handing off to exec thread, + // so the file never contains an entry that is already being sent. + for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { + if (nBestHeight >= it->matureAtHeight) + it = gPendingPayouts.erase(it); else - printf("Pool: payout FAILED for %s (will not retry)\n", kv.first.c_str()); + ++it; + } + SavePendingPayouts(); + std::lock_guard lk(gPayoutExecMutex); + for (auto& pp : ready) + gPayoutExecQueue.push_back(std::move(pp)); + } +} + +// Payout execution thread: drains gPayoutExecQueue, calls SendMoney. +// Runs independently of the event loop so slow wallet operations +// (SendMoney takes cs_main) never stall miner I/O. +static void PayoutThreadFn(void*) +{ + gPayoutThreadRunning = true; + LogPrint("payout", "[payout] payout thread started\n"); + + while (!fShutdown && gPoolRunning) { + std::vector batch; + { + std::lock_guard lk(gPayoutExecMutex); + batch.swap(gPayoutExecQueue); + } + + for (auto& pp : batch) { + LogPrint("payout", "[payout] executing payments for block matured at %d" + " (%zu miners)\n", pp.matureAtHeight, pp.amounts.size()); + for (auto& kv : pp.amounts) { + uint160 h160; + if (!AddressToHash160(kv.first, h160)) { + // Should not happen -- bad addresses are filtered in QueuePayouts + LogPrint("payout", "[payout] SKIPPED bad address '%s'\n", + kv.first.c_str()); + continue; + } + CScript sc; + sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; + CWalletTx wtx; + wtx.mapValue["comment"] = strprintf("Pool payout height %d", + pp.matureAtHeight); + if (SendMoney(sc, kv.second, wtx)) + LogPrint("payout", "[payout] paid %s to %s\n", + FormatMoney(kv.second).c_str(), kv.first.c_str()); + else + LogPrint("payout", "[payout] FAILED to pay %s to %s" + " (insufficient funds or wallet locked?)\n", + FormatMoney(kv.second).c_str(), kv.first.c_str()); + } } - it = gPendingPayouts.erase(it); + + Sleep(500); // check for new work every 500ms } + + gPayoutThreadRunning = false; + LogPrint("payout", "[payout] payout thread stopped\n"); } // --------------------------------------------------------------------------- -// Stratum protocol +// GUI stats snapshot -- event loop writes, GUI threads read // --------------------------------------------------------------------------- -static json MakeNotifyParams(const StratumJob& job, bool clean) +struct StatsSnapshot { + int authorizedMiners = 0; + int blocksFound = 0; + uint64 roundShares = 0; + double totalHashRate = 0.0; +}; +static StatsSnapshot gStats; +static std::vector gWorkerStats; +static std::vector gPayoutViews; +static std::mutex gStatsMutex; + +void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate) { - CBlock& b = const_cast(job.block); - unsigned char hdr[80]; - memcpy(hdr, &b.nVersion, 4); - memcpy(hdr+4, &b.hashPrevBlock, 32); - memcpy(hdr+36, &b.hashMerkleRoot, 32); - memcpy(hdr+68, &b.nTime, 4); - memcpy(hdr+72, &b.nBits, 4); - memcpy(hdr+76, &b.nNonce, 4); - unsigned char shareTgt[32]; memcpy(shareTgt, &job.shareTarget, 32); - return json::array({job.jobId, ToHex(hdr,80), b.hashPrevBlock.GetHex(), ToHex(shareTgt,32), clean}); + std::lock_guard lk(gStatsMutex); + authorizedMiners = gStats.authorizedMiners; + blocksFound = gStats.blocksFound; + roundShares = gStats.roundShares; + totalHashRate = gStats.totalHashRate; +} + +void GetPoolWorkerStats(std::vector& out) +{ + std::lock_guard lk(gStatsMutex); + out = gWorkerStats; +} + +void GetPendingPayouts(std::vector& out) +{ + std::lock_guard lk(gStatsMutex); + out = gPayoutViews; } +// --------------------------------------------------------------------------- +// Miner state -- event loop thread only +// --------------------------------------------------------------------------- +static std::atomic gNextExtranonce2{1}; + +// Vardiff target: aim for one share every VARDIFF_TARGET_SECS seconds per miner. +// After VARDIFF_SHARES_PER_CHECK shares, recalculate and adjust if rate is off +// by more than 50%. Shares are the stable unit; difficulty floats to match. +static const int64 VARDIFF_TARGET_SECS = 15; // one share every 15s +static const uint64 VARDIFF_SHARES_PER_CHECK = 5; // re-evaluate every 5 shares +static const double VARDIFF_RETARGET_RATIO = 1.5; // adjust if >50% off target +static const double VARDIFF_MIN = 0.001; +static const double VARDIFF_MAX = 1e12; +// If a miner has gone this long since its vardiff window opened with ZERO +// shares, the normal vardiff retarget (which only fires after +// VARDIFF_SHARES_PER_CHECK shares) can never trigger -- it has no samples to +// act on. This is the residual gap the ShareTargetFromDifficulty fix above +// doesn't cover: even with a correctly-easy diff-1 target, a miner stuck in +// RandomX light mode (or otherwise much slower than expected) can go +// indefinitely without a single share, and difficulty would never drop to +// meet it. Force a difficulty cut on a timeout instead of waiting for shares +// that will never come. +static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance +static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much + struct Miner { - SOCKET fd; - bool authorised; - std::string address; - std::string worker; - std::string readBuf; - int64 connectTime; - uint64 sessionShares; - Miner(SOCKET f) : fd(f), authorised(false), connectTime(GetTime()), sessionShares(0) {} + btf_socket_t fd; + bool authorised = false; + std::string address; + std::string worker; + std::string readBuf; + int64 connectTime; + uint64 sessionShares = 0; + uint64 roundShares = 0; + uint64 totalShares = 0; + int64 lastSeen = 0; + uint32_t extranonce2; + + // Vardiff state + double difficulty = 1.0; // current share difficulty sent to this miner + uint256 shareTarget; // derived from difficulty; what we check against + int64 vardiffWindowStart = 0; // when the current vardiff window opened + uint64 vardiffShares = 0; // shares found in current window + + // Hashrate estimate -- EWMA of (expected hashes per share at current + // difficulty) / (seconds since the previous share). We had all the data + // to compute this (difficulty + share timing) but never actually did, + // so pool/GUI hashrate always showed as zero/missing. + int64 lastShareTime = 0; // GetTime() of the previous accepted share + double hashRate = 0.0; // smoothed H/s estimate + + explicit Miner(btf_socket_t f) + : fd(f), connectTime(GetTime()), + extranonce2(gNextExtranonce2.fetch_add(1)) {} }; -void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares) +static std::vector gMiners; + +struct FoundBlock { int height; int64 ts; std::string addr; }; +static std::deque gFoundBlocks; + +// --------------------------------------------------------------------------- +// Accept queue -- accept threads push; event loop pops at top of each tick +// --------------------------------------------------------------------------- +static std::vector gReadyQueue; +static std::mutex gReadyMutex; + +static void PushReady(btf_socket_t s) { - authorizedMiners = 0; - blocksFound = 0; - roundShares = 0; - { - std::lock_guard lk(gMinersMutex); - for (Miner* m : gMiners) - if (m->authorised) - authorizedMiners++; + std::lock_guard lk(gReadyMutex); + gReadyQueue.push_back(s); +} + +// --------------------------------------------------------------------------- +// I/O helpers -- event loop thread only +// --------------------------------------------------------------------------- + +// Blocking send of a complete JSON line. Called only from the event loop so +// no other thread is writing to the same fd concurrently. +static bool SendLine(btf_socket_t fd, const json& j) +{ + std::string s = j.dump() + "\n"; + int off = 0, total = (int)s.size(); + while (off < total) { + int r = send(fd, s.c_str() + off, total - off, SEND_FLAGS); + if (r <= 0) return false; + off += r; } - { - std::lock_guard lk(gBlocksMutex); - blocksFound = (int)gFoundBlocks.size(); + return true; +} + +// Non-blocking recv: pull whatever bytes are available now into buf, then +// return one complete line if one is ready. Three outcomes: +// true -- line is filled; buf may still contain more lines +// false, errno EAGAIN/EWOULDBLOCK (or FIONREAD==0) -- no data right now +// false, r==0 -- clean disconnect; caller must treat fd as dead +// Caller distinguishes disconnect from "not yet" by checking whether +// line.empty() after a false return (line is "" on both paths, so we +// use a separate out-param for the disconnect signal). +static bool RecvLine(btf_socket_t fd, std::string& buf, + std::string& line, bool& disconnected) +{ + disconnected = false; + char tmp[4096]; + +#ifdef _WIN32 + u_long avail = 0; + ioctlsocket(fd, FIONREAD, &avail); + if (avail > 0) { + int r = recv(fd, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0){ disconnected = true; return false; } + // r < 0: WSAEWOULDBLOCK -- fall through with what we have } +#else { - std::lock_guard lk(gMinerStatsMutex); - for (std::map::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - roundShares += it->second.roundShares; + int r = recv(fd, tmp, sizeof(tmp) - 1, MSG_DONTWAIT); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + // r < 0: EAGAIN/EWOULDBLOCK -- fall through with what we have } +#endif + + size_t pos = buf.find('\n'); + if (pos == std::string::npos) return false; // no complete line yet + + line = buf.substr(0, pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + buf = buf.substr(pos + 1); + return true; } -static bool SendLine(Miner* m, const json& j) +// --------------------------------------------------------------------------- +// Job helpers +// --------------------------------------------------------------------------- +static CBlock BuildMinerBlock(const StratumJob& job, uint32_t extranonce2) { - std::string s = j.dump() + "\n"; - int off = 0; - while (off < (int)s.size()) { - int n = send(m->fd, s.c_str() + off, (int)s.size() - off, SEND_FLAGS); - if (n <= 0) - return false; - off += n; - } - return true; + CBlock blk = job.block; + blk.vtx[0].vin[0].scriptSig << (CBigNum)extranonce2; + blk.hashMerkleRoot = blk.BuildMerkleTree(); + return blk; +} + +// shareTarget is per-miner (set by vardiff) and passed explicitly so the +// value in p[3] of mining.notify always matches what HandleLine checks on submit. +// This is the single source of truth for share difficulty. +static json MakeNotifyParams(const StratumJob& job, const CBlock& b, + const uint256& shareTarget, bool clean) +{ + unsigned char hdr[80]; + memcpy(hdr, &b.nVersion, 4); + memcpy(hdr+4, &b.hashPrevBlock, 32); + memcpy(hdr+36, &b.hashMerkleRoot, 32); + memcpy(hdr+68, &b.nTime, 4); + memcpy(hdr+72, &b.nBits, 4); + memcpy(hdr+76, &b.nNonce, 4); + unsigned char tgt[32]; memcpy(tgt, &shareTarget, 32); + return json::array({job.jobId, ToHex(hdr, 80), + b.hashPrevBlock.GetHex(), ToHex(tgt, 32), clean}); +} + +static void SendJob(Miner* m, bool clean) +{ + if (!gHaveJob) return; + CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); + // Use this miner's own shareTarget (set by vardiff) in the notify params. + // The participant miner reads p[3] as its share target, and HandleLine + // checks m->shareTarget on submit -- both sides now use the same value. + json n = {{"id",nullptr},{"method","mining.notify"}, + {"params", MakeNotifyParams(gCurrentJob, mb, m->shareTarget, clean)}}; + if (!SendLine(m->fd, n)) + LogPrint("worker", "[pool->worker] job send FAILED for %s -- miner will be dropped\n", + m->address.empty() ? "(not yet authorized)" : m->address.c_str()); } static void BroadcastJob(bool clean) { - StratumJob job; - { std::lock_guard lk(gJobMutex); if(!gHaveJob) return; job=gCurrentJob; } - json n = {{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,clean)}}; - std::lock_guard lk(gMinersMutex); - for (Miner* m : gMiners) - if (m->authorised && !SendLine(m, n)) - printf("Stratum: failed to broadcast job to %s\n", m->address.c_str()); + if (!gHaveJob) return; + int sent = 0; + for (Miner* m : gMiners) { + if (!m->authorised) continue; + SendJob(m, clean); + sent++; + } + LogPrint("pool", "[pool] job %s broadcast to %d miner(s)\n", + gCurrentJob.jobId.c_str(), sent); } -static void HandleLine(Miner* m, const std::string& line) +// --------------------------------------------------------------------------- +// Stratum message handler -- called for each complete line from a miner +// Returns false if the miner should be disconnected immediately. +// --------------------------------------------------------------------------- +static bool HandleLine(Miner* m, const std::string& rawLine, + std::map& roundShareCount, + uint64& roundShareTotal, + int& blocksFoundThisSession) { json req; - try { req = json::parse(line); } catch(...){ return; } + try { req = json::parse(rawLine); } + catch (...) { + LogPrint("worker", "[worker] JSON parse error from %s: %.80s\n", + m->address.empty() ? "(unauth)" : m->address.c_str(), + rawLine.c_str()); + return true; // don't disconnect on bad JSON, just ignore the line + } - json id = req.value("id", json(nullptr)); - std::string method = req.value("method",""); + json id = req.value("id", json(nullptr)); + std::string method = req.value("method", ""); - auto reply = [&](json result, json error=nullptr){ - if (!SendLine(m, json{{"id",id},{"result",result},{"error",error}})) - printf("Stratum: failed to reply to miner %s\n", m->address.c_str()); + // Reply helper: always sends result+error pair as Stratum requires + auto reply = [&](json result, std::string error = "") { + json err = error.empty() ? json(nullptr) : json(error); + if (!SendLine(m->fd, json{{"id",id},{"result",result},{"error",err}})) + LogPrint("worker", "[pool->worker] reply send FAILED for %s (method=%s)\n", + m->address.empty() ? "(unauth)" : m->address.c_str(), + method.c_str()); }; + // ------------------------------------------------------------------ if (method == "mining.subscribe") { std::string sid = ToHex(&m->fd, 4); - printf("Stratum: miner subscribed %s\n", sid.c_str()); + LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n", + (int)m->fd, sid.c_str()); reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4})); - return; + return true; } + // ------------------------------------------------------------------ if (method == "mining.authorize") { auto& p = req["params"]; - m->address = (p.size()>0 && p[0].is_string()) ? p[0].get() : ""; - m->worker = (p.size()>1 && p[1].is_string()) ? p[1].get() : "worker"; - m->authorised = true; - printf("Stratum: miner authorized address=%s worker=%s\n", m->address.c_str(), m->worker.c_str()); - reply(true); + m->address = (p.size() > 0 && p[0].is_string()) ? p[0].get() : ""; + m->worker = (p.size() > 1 && p[1].is_string()) ? p[1].get() : "worker"; + m->lastSeen = GetTime(); - uint160 h; bool valid = AddressToHash160(m->address, h); - if (!valid) printf("Stratum: miner %s invalid address '%s' — shares won't pay out\n", - m->worker.c_str(), m->address.c_str()); + // Validate address before authorising + uint160 h160; bool validAddr = AddressToHash160(m->address, h160); - { std::lock_guard lk(gMinerStatsMutex); - auto& ms = gMinerStats[m->address]; - ms.lastSeen = GetTime(); ms.worker = m->worker; } + m->authorised = true; // authorise regardless -- shares just won't pay out + LogPrint("worker", "[worker->pool] authorize address=%s worker=%s addr_valid=%s\n", + m->address.c_str(), m->worker.c_str(), validAddr ? "yes" : "NO"); - if (!SendLine(m, json{{"id",nullptr},{"method","mining.set_difficulty"},{"params",json::array({1})}})) - printf("Stratum: failed to send difficulty to %s\n", m->address.c_str()); + if (!validAddr) + LogPrint("worker", "[pool] WARNING: address '%s' is invalid -- " + "miner will hash but shares cannot be paid out\n", + m->address.c_str()); - StratumJob job; bool have; - { std::lock_guard lk(gJobMutex); have=gHaveJob; if(have) job=gCurrentJob; } - if (have) { - printf("Stratum: sending job %s to %s\n", job.jobId.c_str(), m->address.c_str()); - if (!SendLine(m, json{{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,true)}})) - printf("Stratum: failed to send job %s to %s\n", job.jobId.c_str(), m->address.c_str()); + reply(true); + + // Initialize vardiff state and send initial difficulty + m->difficulty = 1.0; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + m->vardiffWindowStart = GetTime(); + m->vardiffShares = 0; + LogPrint("worker", "[pool->worker] initial difficulty=%.4f for %s\n", + m->difficulty, m->address.c_str()); + if (!SendLine(m->fd, json{{"id",nullptr},{"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}})) + LogPrint("worker", "[pool->worker] set_difficulty send FAILED for %s\n", + m->address.c_str()); + + if (gHaveJob) { + LogPrint("worker", "[pool->worker] sending job %s to %s\n", + gCurrentJob.jobId.c_str(), m->address.c_str()); + SendJob(m, true); } else { - printf("Stratum: no job available for %s\n", m->address.c_str()); + LogPrint("worker", "[pool->worker] no job available yet for %s" + " -- will be sent one as soon as the pool builds it\n", + m->address.c_str()); } - return; + return true; } - if (method == "mining.extranonce.subscribe") { reply(true); return; } + // ------------------------------------------------------------------ + if (method == "mining.extranonce.subscribe") { reply(true); return true; } + if (method == "mining.get_transactions") { reply(json::array()); return true; } + // ------------------------------------------------------------------ if (method == "mining.submit") { - if (!m->authorised) { reply(false,"not authorised"); return; } + if (!m->authorised) { + LogPrint("worker", "[worker->pool] submit from unauthorized miner (fd=%d)\n", + (int)m->fd); + reply(false, "not authorised"); + return true; + } auto& p = req["params"]; - if (p.size() < 4) { reply(false,"bad params"); return; } + if (p.size() < 3) { + LogPrint("worker", "[worker->pool] submit bad params from %s (size=%zu)\n", + m->address.c_str(), p.size()); + reply(false, "bad params"); + return true; + } - std::string jobId = p[1].get(); - std::string nonceHex = p[2].get(); + std::string submitJobId = p[1].get(); + std::string nonceHex = p[2].get(); - StratumJob job; - { std::lock_guard lk(gJobMutex); - if (!gHaveJob || gCurrentJob.jobId != jobId) { reply(false,"stale job"); return; } - job = gCurrentJob; } + if (!gHaveJob) { + LogPrint("worker", "[worker->pool] submit from %s but pool has no current job\n", + m->address.c_str()); + reply(false, "stale job"); + return true; + } + if (gCurrentJob.jobId != submitJobId) { + LogPrint("worker", "[worker->pool] submit stale job from %s" + " (submitted=%s current=%s)\n", + m->address.c_str(), submitJobId.c_str(), gCurrentJob.jobId.c_str()); + reply(false, "stale job"); + return true; + } auto nb = FromHex(nonceHex); - if (nb.size()<4) { reply(false,"bad nonce"); return; } + if (nb.size() < 4) { + LogPrint("worker", "[worker->pool] submit bad nonce from %s: '%s'\n", + m->address.c_str(), nonceHex.c_str()); + reply(false, "bad nonce"); + return true; + } unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); - CBlock& b = job.block; + // Rebuild this miner's exact block (unique merkle via extranonce2) + CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); b.nNonce = nNonce; unsigned char hdr[80]; - memcpy(hdr, &b.nVersion, 4); - memcpy(hdr+4, &b.hashPrevBlock, 32); - memcpy(hdr+36, &b.hashMerkleRoot,32); - memcpy(hdr+68, &b.nTime, 4); - memcpy(hdr+72, &b.nBits, 4); - memcpy(hdr+76, &b.nNonce, 4); + memcpy(hdr, &b.nVersion, 4); + memcpy(hdr+4, &b.hashPrevBlock, 32); + memcpy(hdr+36, &b.hashMerkleRoot, 32); + memcpy(hdr+68, &b.nTime, 4); + memcpy(hdr+72, &b.nBits, 4); + memcpy(hdr+76, &b.nNonce, 4); uint256 powHash = RandomXPoWHash(hdr, 80); - // Record share in the current round. - std::string key = MinerKey(m->address, m->worker); - { std::lock_guard lk(gMinerStatsMutex); - MinerStats& ms = gMinerStats[key]; - ms.address = m->address; - ms.worker = m->worker; - ms.roundShares++; - ms.totalShares++; - ms.lastSeen = GetTime(); } + // Check against this miner's personal share target (set by vardiff) + if (powHash > m->shareTarget) { + LogPrint("worker", "[worker->pool] high-hash from %s" + " nonce=%u (difficulty=%.4f)\n", + m->address.c_str(), nNonce, m->difficulty); + reply(false, "high-hash"); + return true; + } + + // Valid share -- record it m->sessionShares++; + m->roundShares++; + m->totalShares++; + m->vardiffShares++; + int64 shareTime = GetTime(); + m->lastSeen = shareTime; + roundShareCount[m->address]++; + roundShareTotal++; + + // Update hashrate estimate: expected hashes for this share divided by + // the time since the last one, smoothed with an EWMA so a single fast + // or slow share doesn't make the number jump around. + if (m->lastShareTime > 0) { + int64 dt = shareTime - m->lastShareTime; + if (dt <= 0) dt = 1; + double inst = ExpectedHashesForDifficulty(m->difficulty) / (double)dt; + const double alpha = 0.25; + m->hashRate = (m->hashRate <= 0.0) ? inst + : alpha * inst + (1.0 - alpha) * m->hashRate; + } + m->lastShareTime = shareTime; - if (powHash > job.shareTarget) { reply(true); return; } // valid share, not a full block + LogPrint("worker", "[worker->pool] valid share from %s" + " nonce=%u job=%s difficulty=%.4f (session: %llu)\n", + m->address.c_str(), nNonce, + gCurrentJob.jobId.c_str(), m->difficulty, + (unsigned long long)m->sessionShares); + reply(true); + + // Vardiff: after every VARDIFF_SHARES_PER_CHECK shares, check rate + if (m->vardiffShares >= VARDIFF_SHARES_PER_CHECK) { + int64 elapsed = GetTime() - m->vardiffWindowStart; + if (elapsed <= 0) elapsed = 1; + double actualSecsPerShare = (double)elapsed / (double)m->vardiffShares; + double ratio = actualSecsPerShare / (double)VARDIFF_TARGET_SECS; + // ratio < 1 means shares arriving faster than target (too easy) + // ratio > 1 means shares arriving slower than target (too hard) + if (ratio < (1.0 / VARDIFF_RETARGET_RATIO) || + ratio > VARDIFF_RETARGET_RATIO) { + // New difficulty scales inversely with actual share rate + double newDiff = m->difficulty / ratio; + if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN; + if (newDiff > VARDIFF_MAX) newDiff = VARDIFF_MAX; + // Only apply if the change is meaningful (>10%) + if (newDiff / m->difficulty > 1.1 || m->difficulty / newDiff > 1.1) { + m->difficulty = newDiff; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + LogPrint("worker", "[pool] vardiff: %s new difficulty=%.4f" + " (%.1fs/share actual vs %ds target)\n", + m->address.c_str(), m->difficulty, + actualSecsPerShare, (int)VARDIFF_TARGET_SECS); + SendLine(m->fd, json{{"id",nullptr}, + {"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}}); + // set_difficulty alone doesn't change what the worker + // actually hashes against -- it only reads its share + // target from mining.notify params[3]. Without this, the + // new difficulty wouldn't take effect until the next + // natural job broadcast (new block, or the 60s trickle + // refresh), so push a fresh notify for this miner now. + SendJob(m, false); + } + } + // Reset window + m->vardiffWindowStart = GetTime(); + m->vardiffShares = 0; + } + + if (powHash > gCurrentJob.target) return true; // valid share, not a block + + // Full difficulty met -- submit block to the chain + LogPrint("pool", "[pool] BLOCK CANDIDATE from %s height=%d nNonce=%u -- submitting\n", + m->address.c_str(), gCurrentJob.height, nNonce); - // Full block CBlock* pblock = new CBlock(b); bool accepted = false; CRITICAL_BLOCK(cs_main) { + int heightBefore = nBestHeight; if (ProcessBlock(NULL, pblock)) { - accepted = true; - printf("Pool: BLOCK FOUND by %s height=%d nNonce=%u\n", - m->address.c_str(), job.height, nNonce); - FoundBlock fb{job.height, GetTime(), m->address, pblock->GetHash()}; - { std::lock_guard lk(gBlocksMutex); - gFoundBlocks.push_front(fb); - if (gFoundBlocks.size()>50) gFoundBlocks.pop_back(); } - { std::lock_guard lk(gJobMutex); gHaveJob=false; } + // ProcessBlock returns true for orphans too -- verify the chain + // actually advanced. If nBestHeight didn't increase, the block + // was shelved as an orphan (pool was behind the chain tip) and + // no coinbase was added to the wallet. Do not pay out. + if (nBestHeight > heightBefore) { + accepted = true; + LogPrint("pool", "[pool] BLOCK ACCEPTED height=%d by %s\n", + gCurrentJob.height, m->address.c_str()); + gFoundBlocks.push_front({gCurrentJob.height, GetTime(), m->address}); + if (gFoundBlocks.size() > 50) gFoundBlocks.pop_back(); + blocksFoundThisSession++; + gHaveJob = false; // invalidate job immediately + } else { + LogPrint("pool", "[pool] BLOCK ORPHANED height=%d by %s" + " -- pool was behind chain tip, no payout\n", + gCurrentJob.height, m->address.c_str()); + gHaveJob = false; // job is stale regardless + } } else { + LogPrint("pool", "[pool] BLOCK REJECTED height=%d (ProcessBlock returned false)\n", + gCurrentJob.height); delete pblock; } } - reply(accepted); - if (accepted) { QueuePayouts(job.height); RebuildJob(); BroadcastJob(true); } - return; + + if (accepted) { + // Snapshot height before RebuildJob() (which increments it) + int foundHeight = gCurrentJob.height; + QueuePayouts(foundHeight, roundShareCount, roundShareTotal); + // Reset round shares + roundShareCount.clear(); + roundShareTotal = 0; + for (Miner* mi : gMiners) mi->roundShares = 0; + // Build and broadcast new job immediately + if (RebuildJob()) { + LogPrint("pool", "[pool] new job %s built after block %d\n", + gCurrentJob.jobId.c_str(), foundHeight); + BroadcastJob(true); + } else { + LogPrint("pool", "[pool] WARNING: RebuildJob() failed after block %d" + " -- miners will wait\n", foundHeight); + } + } + return true; } - if (method == "mining.get_transactions") { reply(json::array()); return; } + // Unknown method -- log it (can reveal miner software quirks) + LogPrint("worker", "[worker->pool] unknown method '%s' from %s\n", + method.c_str(), + m->address.empty() ? "(unauth)" : m->address.c_str()); reply(nullptr, "unknown method"); + return true; } -static void MinerThreadFn(void* arg) +// --------------------------------------------------------------------------- +// Stats refresh -- called every 2s from event loop +// --------------------------------------------------------------------------- +static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) { - Miner* m = (Miner*)arg; - while (!fShutdown && gPoolRunning) { - fd_set fds; FD_ZERO(&fds); FD_SET(m->fd, &fds); - struct timeval tv={1,0}; - if (select((int)m->fd+1, &fds, NULL, NULL, &tv) <= 0) continue; - char buf[4096]; - int n = recv(m->fd, buf, sizeof(buf)-1, 0); - if (n <= 0) break; - buf[n]=0; m->readBuf += buf; - size_t pos; - while ((pos = m->readBuf.find('\n')) != std::string::npos) { - std::string line = m->readBuf.substr(0, pos); - m->readBuf = m->readBuf.substr(pos+1); - if (!line.empty() && line.back()=='\r') line.pop_back(); - if (!line.empty()) HandleLine(m, line); - } + std::lock_guard lk(gStatsMutex); + + gStats.authorizedMiners = 0; + gStats.blocksFound = blocksFoundThisSession; + gStats.roundShares = roundShareTotal; + gStats.totalHashRate = 0.0; + for (Miner* m : gMiners) + if (m->authorised) gStats.authorizedMiners++; + + gWorkerStats.clear(); + for (Miner* m : gMiners) { + if (!m->authorised || m->lastSeen <= 0) continue; + PoolWorkerStatView v; + v.address = m->address; + v.worker = m->worker; + v.roundShares = m->roundShares; + v.totalShares = m->totalShares; + v.lastSeen = m->lastSeen; + v.hashRate = m->hashRate; + gWorkerStats.push_back(v); + gStats.totalHashRate += m->hashRate; + } + std::sort(gWorkerStats.begin(), gWorkerStats.end(), + [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { + return a.roundShares != b.roundShares ? a.roundShares > b.roundShares + : a.lastSeen > b.lastSeen; + }); + + // gPendingPayouts is event-loop-only data (no lock needed here since + // RefreshStats is always called from the event loop thread). + gPayoutViews.clear(); + for (const PendingPayout& pp : gPendingPayouts) { + PendingPayoutView pv; + pv.matureAtHeight = pp.matureAtHeight; + pv.recipients = (int)pp.amounts.size(); + pv.totalAmount = 0; + for (auto& kv : pp.amounts) pv.totalAmount += kv.second; + gPayoutViews.push_back(pv); } - sock_close(m->fd); - printf("Stratum: miner %s disconnected (%llu shares)\n", - m->worker.empty()?"unknown":m->worker.c_str(), - (unsigned long long)m->sessionShares); - { std::lock_guard lk(gMinersMutex); - auto it = std::find(gMiners.begin(), gMiners.end(), m); - if (it != gMiners.end()) gMiners.erase(it); } - delete m; } -static void JobWatcherFn(void*) +// --------------------------------------------------------------------------- +// Event loop +// --------------------------------------------------------------------------- +static void PoolEventLoop() { - int lastHeight = -1; + int lastHeight = -1; + int64 lastRefresh = GetTime(); + int64 lastStatsRefresh = 0; + int blocksFoundThisSession = 0; + int64 noJobSince = 0; // 0 = have a job + bool warnedNoJob = false; + int64 lastNoJobRetry = 0; // throttle RebuildJob calls when no tip + + std::map roundShareCount; + uint64 roundShareTotal = 0; + + static const int64 JOB_REFRESH_SECS = 60; // re-stamp nTime periodically + static const int64 NO_JOB_WARN_SECS = 15; // warn after this long with no job + static const int64 NO_JOB_RETRY_SECS = 5; // how often to retry RebuildJob + while (!fShutdown && gPoolRunning) { - Sleep(500); - bool newBlock = false; - CRITICAL_BLOCK(cs_main) - { if (nBestHeight != lastHeight && pindexBest) { lastHeight = nBestHeight; newBlock = true; } } - if (newBlock) { - FlushMaturePayouts(); - if (RebuildJob()) { printf("Stratum: new job height=%d\n", lastHeight+1); BroadcastJob(true); } - } else if (!gHaveJob) { + + // 1. Drain accept queue + { + std::lock_guard lk(gReadyMutex); + for (btf_socket_t fd : gReadyQueue) { + gMiners.push_back(new Miner(fd)); + LogPrint("worker", "[pool] miner fd=%d entered event loop" + " -- awaiting subscribe/authorize\n", (int)fd); + } + gReadyQueue.clear(); + } + + // 2. Detect new block. + // When our own miner found the block, HandleLine already called + // RebuildJob() + BroadcastJob(true) and set gHaveJob=true. + // We still flush mature payouts and update bookkeeping, but skip + // rebuilding if a fresh job already exists for this height. + { + bool newBlock = false; + CRITICAL_BLOCK(cs_main) + { + if (pindexBest && nBestHeight != lastHeight) { + lastHeight = nBestHeight; + newBlock = true; + } + } + if (newBlock) { + FlushMaturePayouts(); + if (!gHaveJob) { + // External block (not found by our miners), or HandleLine's + // RebuildJob failed. Either way this round is over with no + // payout from us, so clear round shares before starting the + // next one -- otherwise they'd keep accumulating across + // rounds that our own miners didn't win. + roundShareCount.clear(); + roundShareTotal = 0; + for (Miner* mi : gMiners) mi->roundShares = 0; + + // Build and broadcast now. + if (RebuildJob()) { + LogPrint("pool", "[pool] new block height=%d -- job %s ready," + " %zu miner(s) connected\n", + lastHeight, gCurrentJob.jobId.c_str(), gMiners.size()); + BroadcastJob(true); + } else { + LogPrint("pool", "[pool] new block height=%d but RebuildJob() failed" + " -- miners will wait\n", lastHeight); + } + } else { + // HandleLine already built and broadcast for this height. + LogPrint("pool", "[pool] new block height=%d -- job already" + " broadcast by submit handler\n", lastHeight); + } + lastRefresh = GetTime(); + noJobSince = 0; + warnedNoJob = false; + } + } + + // 3. No-job watchdog (throttled: only retry every NO_JOB_RETRY_SECS) + if (!gHaveJob) { + int64 now = GetTime(); + if (noJobSince == 0) noJobSince = now; + + if (now - lastNoJobRetry >= NO_JOB_RETRY_SECS) { + lastNoJobRetry = now; + if (RebuildJob()) { + LogPrint("pool", "[pool] job built after %llds without one\n", + (long long)(now - noJobSince)); + BroadcastJob(false); + noJobSince = 0; + warnedNoJob = false; + lastRefresh = now; + } else if (!warnedNoJob && now - noJobSince >= NO_JOB_WARN_SECS) { + LogPrint("pool", "[pool] STUCK: no job for %llds -- %s." + " Miners are waiting. Will keep retrying every %llds.\n", + (long long)(now - noJobSince), + pindexBest ? "RebuildJob() keeps failing (check above for why)" + : "no chain tip yet (node still syncing)", + (long long)NO_JOB_RETRY_SECS); + warnedNoJob = true; + } + } + } else if (GetTime() - lastRefresh >= JOB_REFRESH_SECS) { + // Periodic nTime refresh -- stale timestamps cause valid hashes to be rejected if (RebuildJob()) BroadcastJob(false); + lastRefresh = GetTime(); + } + + // 3.5. Vardiff stall watchdog -- runs independently of share count. + // The normal vardiff check in HandleLine only fires once + // VARDIFF_SHARES_PER_CHECK shares have arrived; a miner that never + // finds a single share never reaches that check, so it can be stuck + // at its initial (or last) difficulty forever. Catch that here. + for (Miner* m : gMiners) { + if (!m->authorised || m->vardiffShares != 0 || m->vardiffWindowStart <= 0) + continue; + int64 stalled = GetTime() - m->vardiffWindowStart; + if (stalled < VARDIFF_STALL_SECS) continue; + + double newDiff = m->difficulty / VARDIFF_STALL_CUT; + if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN; + + if (newDiff < m->difficulty) { + m->difficulty = newDiff; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + m->vardiffWindowStart = GetTime(); + LogPrint("worker", "[pool] vardiff STALL: %s sent zero shares in" + " %llds -- forcing difficulty down to %.6f (no share data" + " to retarget from normally)\n", + m->address.c_str(), (long long)stalled, m->difficulty); + SendLine(m->fd, json{{"id",nullptr}, + {"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}}); + // Same reasoning as the normal vardiff retarget above: the + // worker only reads its share target from mining.notify, so + // push a fresh job now instead of waiting on the next + // natural broadcast. + SendJob(m, false); + } else { + // Already at the difficulty floor -- this miner's problem isn't + // difficulty, it's something else (slow hashing, bad connection, + // stuck RandomX light mode, etc). Log once per stall window so + // it's visible without spamming. + LogPrint("worker", "[pool] vardiff STALL: %s still at zero shares" + " after %llds at floor difficulty %.6f -- likely a" + " hashrate/connectivity problem, not a difficulty problem\n", + m->address.c_str(), (long long)stalled, m->difficulty); + m->vardiffWindowStart = GetTime(); // avoid re-logging every tick + } + } + + // 4. Select over miner sockets + if (!gMiners.empty()) { + fd_set fds; FD_ZERO(&fds); + int maxfd = 0; + for (Miner* m : gMiners) { + FD_SET(m->fd, &fds); + if ((int)m->fd > maxfd) maxfd = (int)m->fd; + } + struct timeval tv = {0, 50000}; // 50ms + if (select(maxfd + 1, &fds, NULL, NULL, &tv) > 0) { + std::vector dead; + for (Miner* m : gMiners) { + if (!FD_ISSET(m->fd, &fds)) continue; + std::string line; + bool disc = false; + // Drain all complete lines buffered for this miner. + // HandleLine always returns true; disc signals clean disconnect. + while (RecvLine(m->fd, m->readBuf, line, disc)) + HandleLine(m, line, roundShareCount, + roundShareTotal, blocksFoundThisSession); + if (disc) dead.push_back(m); + } + for (Miner* m : dead) { + LogPrint("worker", "[pool] miner %s disconnected" + " (session shares: %llu, round shares: %llu)\n", + m->worker.empty() ? "(never authorized)" : m->worker.c_str(), + (unsigned long long)m->sessionShares, + (unsigned long long)m->roundShares); + sock_close(m->fd); + gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m)); + delete m; + } + } + } else { + Sleep(50); + } + + // 5. Refresh GUI stats every 2s + if (GetTime() - lastStatsRefresh >= 2) { + RefreshStats(blocksFoundThisSession, roundShareTotal); + lastStatsRefresh = GetTime(); } } + + for (Miner* m : gMiners) { sock_close(m->fd); delete m; } + gMiners.clear(); } // --------------------------------------------------------------------------- -// Main Stratum accept loop — entry point called from ui.cpp and headless.cpp -// --------------------------------------------------------------------------- -// .btf Stratum listener — miners connect via rendezvous relay using the -// node's .btf address instead of a raw IP:port. Same Stratum protocol, -// same MinerThreadFn handler, works through CGNAT with no port forwarding. -// -// Keeps one listener registered per relay at all times. When a miner -// connects, a replacement listener is spawned immediately so the next -// miner doesn't have to wait. +// Accept threads // --------------------------------------------------------------------------- +struct AcceptCtx { unsigned char pk[32]; unsigned char sk[32]; std::string relay; }; -struct BtfAcceptCtx { unsigned char pk[32]; unsigned char sk[32]; string relay; }; - -// Track active listener count per relay to avoid pile-up when relay is slow -static std::map gBtfListenerCount; -static std::mutex gBtfListenerMutex; - -static void BtfStratumAcceptOneFn(void* arg) +// One persistent thread per relay. Loops: register -> wrap -> push -> repeat. +static void AcceptOneFn(void* arg) { - BtfAcceptCtx* ctx = (BtfAcceptCtx*)arg; + AcceptCtx* ctx = (AcceptCtx*)arg; unsigned char pk[32], sk[32]; memcpy(pk, ctx->pk, 32); memcpy(sk, ctx->sk, 32); - string relay = ctx->relay; + std::string relay = ctx->relay; delete ctx; - auto decrement = [&]() { - std::lock_guard lk(gBtfListenerMutex); - gBtfListenerCount[relay]--; - }; - size_t colon = relay.rfind(':'); - if (colon == string::npos) { decrement(); return; } - string strHost = relay.substr(0, colon); - int nPort = atoi(relay.substr(colon+1).c_str()); - - btf::RvSocket rv = btf::RvServiceRegister(strHost.c_str(), (unsigned short)nPort, pk); - if (fShutdown || !gPoolRunning) { if (rv != btf::RV_INVALID) btf::RvClose(rv); decrement(); return; } - if (rv == btf::RV_INVALID) { decrement(); return; } - - // Paired: spawn replacement (count transfers to new thread) - if (!fShutdown && gPoolRunning) { - BtfAcceptCtx* next = new BtfAcceptCtx(); - memcpy(next->pk, pk, 32); memcpy(next->sk, sk, 32); - next->relay = relay; - LaunchThread(BtfStratumAcceptOneFn, next); - } else { - decrement(); + if (colon == std::string::npos) { + LogPrint("pool", "[pool] AcceptOneFn: bad relay address '%s'\n", relay.c_str()); + return; } + std::string host = relay.substr(0, colon); + int port = atoi(relay.substr(colon + 1).c_str()); + + LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); + + while (!fShutdown && gPoolRunning) { + // Blocks until a miner connects to us at this relay + btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); - btf_socket_t hSocket = btf::BtfServiceWrap(rv, sk); - if (hSocket == INVALID_SOCKET) return; + if (fShutdown || !gPoolRunning) { + if (rv != btf::RV_INVALID) btf::RvClose(rv); + break; + } + if (rv == btf::RV_INVALID) { + LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" + " -- backing off 30s before retry\n", relay.c_str()); + for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); + continue; + } - printf("Stratum/.btf: miner connected via %s\n", relay.c_str()); - Miner* m = new Miner(hSocket); - { std::lock_guard lk(gMinersMutex); gMiners.push_back(m); } - LaunchThread(MinerThreadFn, m); + // Echan handshake -- blocking but fast (local + relay round-trip) + btf_socket_t fd = btf::BtfServiceWrap(rv, sk); + if (fd == INVALID_SOCKET) { + LogPrint("pool", "[pool] relay %s: BtfServiceWrap failed (echan handshake)" + " -- miner dropped, will accept next\n", relay.c_str()); + continue; + } + + LogPrint("worker", "[pool] miner arrived via relay %s -- pushing to event loop\n", + relay.c_str()); + PushReady(fd); + // Loop immediately: register again so the next miner can connect + } + + LogPrint("pool", "[pool] accept thread stopped for relay %s\n", relay.c_str()); } -static void BtfStratumAcceptFn(void*) +// Spawns one AcceptOneFn per relay. Re-polls relay list every 60s to pick up +// newly discovered relays. Uses a set to avoid spawning duplicate threads. +static void AcceptLoopFn(void*) { - printf("Stratum/.btf: starting \xe2\x80\x94 miners can connect via %s\n", - BtfLocalAddress().c_str()); + LogPrint("pool", "[pool] AcceptLoopFn started -- miners can connect via %s\n", + BtfLocalAddress().c_str()); + // Wait for our identity to be ready unsigned char pk[32], sk[32]; + int waitSecs = 0; while (!BtfGetIdentity(pk, sk)) { if (fShutdown || !gPoolRunning) return; + if (waitSecs == 0) + LogPrint("pool", "[pool] waiting for .btf identity...\n"); + waitSecs++; Sleep(2000); } + LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); + + std::set spawned; // relays we've already started a thread for while (!fShutdown && gPoolRunning) { vector relays = BtfAllRelays(); - if (relays.empty()) { Sleep(5000); continue; } - - // Spawn one listener per relay, only if none already active - for (const string& relay : relays) { - std::lock_guard lk(gBtfListenerMutex); - if (gBtfListenerCount[relay] == 0) { - gBtfListenerCount[relay]++; - BtfAcceptCtx* ctx = new BtfAcceptCtx(); + if (relays.empty()) { + LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); + } else { + for (const string& relay : relays) { + if (spawned.count(relay)) continue; // already running + AcceptCtx* ctx = new AcceptCtx(); memcpy(ctx->pk, pk, 32); memcpy(ctx->sk, sk, 32); ctx->relay = relay; - LaunchThread(BtfStratumAcceptOneFn, ctx); + uintptr_t tid = _beginthread(AcceptOneFn, 0, ctx); + if (tid == (uintptr_t)-1) { + delete ctx; + LogPrint("pool", "[pool] failed to spawn accept thread for %s\n", + relay.c_str()); + } else { + spawned.insert(relay); + LogPrint("pool", "[pool] accept thread spawned for relay %s" + " (%zu total)\n", relay.c_str(), spawned.size()); + } } } - for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); + // Re-poll every 60s -- newly discovered relays get picked up + for (int i = 0; i < 60 && !fShutdown && gPoolRunning; i++) Sleep(1000); } } + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- void ThreadRPCServer(void*) { gPoolRunning = true; - printf("Bitflash pool server starting\n"); - printf(" Stratum .btf: %s (no port forwarding needed)\n", - BtfLocalAddress().c_str()); - printf(" Mode: operator\n"); - if (RebuildJob()) - printf("Stratum: initial job seeded height=%d\n", nBestHeight + 1); + LogPrint("pool", "[pool] ============================================\n"); + LogPrint("pool", "[pool] Pool server starting\n"); + LogPrint("pool", "[pool] .btf address: %s\n", BtfLocalAddress().c_str()); + LogPrint("pool", "[pool] fee: %.2f%%\n", dPoolFeePercent); + + if (RebuildJob()) { + LogPrint("pool", "[pool] initial job %s seeded at height %d\n", + gCurrentJob.jobId.c_str(), nBestHeight + 1); + } else { + LogPrint("pool", "[pool] no chain tip yet -- job will build on first block\n"); + } + + LoadPendingPayouts(); + + _beginthread(AcceptLoopFn, 0, NULL); + _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); + _beginthread(PayoutThreadFn, 0, NULL); - LaunchThread(JobWatcherFn, NULL); - LaunchThread(BtfStratumAcceptFn, NULL); - LaunchThread(ThreadBtfPoolAnnouncer, NULL); + PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown - while (!fShutdown && gPoolRunning) - Sleep(1000); - printf("Pool server stopped\n"); + LogPrint("pool", "[pool] pool server stopped\n"); + LogPrint("pool", "[pool] ============================================\n"); + gPoolRunning = false; } diff --git a/src/ui.rc b/src/ui.rc deleted file mode 100644 index ec8b142..0000000 --- a/src/ui.rc +++ /dev/null @@ -1,14 +0,0 @@ -bitcoin ICON "rc/bitcoin.ico" - -#include "wx/msw/wx.rc" - -check ICON "rc/check.ico" -send16 BITMAP "rc/send16.bmp" -send16mask BITMAP "rc/send16mask.bmp" -send16masknoshadow BITMAP "rc/send16masknoshadow.bmp" -send20 BITMAP "rc/send20.bmp" -send20mask BITMAP "rc/send20mask.bmp" -addressbook16 BITMAP "rc/addressbook16.bmp" -addressbook16mask BITMAP "rc/addressbook16mask.bmp" -addressbook20 BITMAP "rc/addressbook20.bmp" -addressbook20mask BITMAP "rc/addressbook20mask.bmp" diff --git a/src/uiproject.fbp b/src/uiproject.fbp deleted file mode 100644 index 57a2950..0000000 --- a/src/uiproject.fbp +++ /dev/null @@ -1,11825 +0,0 @@ - - - - - - C++ - 1 - UTF-8 - connect - uibase - 1000 - none - 0 - - - . - - 1 - 0 - 0 - - wxSYS_COLOUR_BTNFACE - - - 1 - - - - 0 - wxID_MAINFRAME - - - CMainFrameBase - - 705,484 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Bitcoin - - - - wxTAB_TRAVERSAL - 1 - - - - OnClose - - - - - OnIdle - - - - - - - - - - - - OnMouseEvents - - OnPaint - - - - - - - - 240,240,240 - - 1 - - - 0 - wxID_ANY - MyMenuBar - - - m_menubar - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - &File - m_menuFile - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - E&xit - m_menuFileExit - none - - - OnMenuFileExit - - - - - &Options - m_menuOptions - public - - - 0 - 1 - - wxID_OPTIONSGENERATEBITCOINS - wxITEM_CHECK - &Generate Coins - m_menuOptionsGenerateBitcoins - none - - - OnMenuOptionsGenerate - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - &Options... - m_menuOptionsOptions - none - - - OnMenuOptionsOptions - - - - - &Help - m_menuHelp - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - &About... - m_menuHelpAbout - none - - - OnMenuHelpAbout - - - - - - - 20,20 - - 1 - - ,90,90,-1,70,0 - 0 - wxID_ANY - - - - m_toolBar - 1 - protected - - 1 - -1,-1 - wxTB_FLAT|wxTB_HORZ_TEXT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - send20; Load From Resource - wxID_BUTTONSEND - wxITEM_NORMAL - &Send Coins - m_tool1 - - - - OnButtonSend - - - - - - addressbook20; Load From Resource - wxID_BUTTONRECEIVE - wxITEM_NORMAL - &Address Book - m_tool2 - - - - OnButtonAddressBook - - - - - - - 240,240,240 - - 1 - - 1 - - 0 - wxID_ANY - - - m_statusBar - protected - - - wxST_SIZEGRIP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer2 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 2 - protected - 0 - - - - 5 - wxEXPAND|wxRIGHT|wxLEFT - 0 - - - bSizer85 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Your Bitcoin Address: - - - m_staticText32 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - wxSYS_COLOUR_MENU - - 1 - - - 0 - wxID_TEXTCTRLADDRESS - -1,-1 - 0 - -1,-1 - m_textCtrlAddress - protected - - 250,-1 - wxTE_READONLY - - - - - - - - - - OnKeyDown - - - - - - - - - - - OnMouseEventsAddress - - - - - - OnSetFocusAddress - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCOPY - &Copy to Clipboard - - - m_buttonCopy - protected - - - wxBU_EXACTFIT - - - - - - OnButtonCopy - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCHANGE - C&hange... - - - m_button91 - protected - - - - - - - - - OnButtonChange - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 0 - - - bSizer3 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxALIGN_BOTTOM|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_panel14 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer66 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Balance: - - - m_staticText41 - protected - - -1,15 - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 240,240,240 - - 1 - - ,90,90,8,70,0 - 0 - wxID_ANY - - - - m_staticTextBalance - protected - - 120,15 - wxALIGN_RIGHT|wxST_NO_AUTORESIZE - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - 5 - wxALIGN_BOTTOM|wxTOP|wxRIGHT|wxLEFT - 0 - - - " All" " Sent" " Received" " In Progress" - - 1 - - - 1 - wxID_ANY - - - m_choiceFilter - protected - - 0 - 110,-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - - - 1 - - - 0 - wxID_ANY - - - m_notebook - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - All Transactions - 0 - - - - 1 - - - 0 - wxID_ANY - - - m_panel7 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer157 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_DESCENDING - - - - - wxALWAYS_SHOW_SB - - - - - - - - - - - - - - - OnListColBeginDrag - - - - - - - - - OnListItemActivatedAllTransactions - - - - - - - - - - - - - OnPaintListCtrl - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer_TabsForFutureUse - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel9 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer159 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlEscrows - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel8 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer158 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlOrdersSent - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedOrdersSent - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel10 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer160 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlProductsSent - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedProductsSent - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel11 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer161 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlOrdersReceived - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedOrdersReceived - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CTxDetailsDialogBase - - 620,450 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Transaction Details - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer64 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer66 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_htmlWin - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer65 - wxVERTICAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - COptionsDialogBase - - 500,261 - wxDEFAULT_DIALOG_STYLE - - Options - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer55 - wxVERTICAL - none - - 5 - wxEXPAND|wxLEFT - 1 - - - bSizer57 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 20 - protected - 0 - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - Optional transaction fee you give to the nodes that process your transactions. - - - m_staticText32 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer56 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Transaction fee: - - - m_staticText31 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_TRANSACTIONFEE - - 0 - - m_textCtrlTransactionFee - protected - - 70,-1 - - - - - - - - - - - - - OnKillFocusTransactionFee - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer58 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CAboutDialogBase - - 507,298 - wxDEFAULT_DIALOG_STYLE - - About Bitcoin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer60 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer62 - wxHORIZONTAL - none - - 5 - wxEXPAND - 0 - - 0 - protected - 60 - - - - 5 - wxEXPAND - 1 - - - bSizer63 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 50 - protected - 0 - - - - 5 - wxEXPAND - 0 - - - bSizer64 - wxHORIZONTAL - none - - 5 - wxALIGN_BOTTOM|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - Tahoma,90,92,10,74,0 - 0 - wxID_ANY - Bitcoin - - - m_staticText40 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_BOTTOM|wxTOP|wxBOTTOM|wxRIGHT - 0 - - - - 1 - - Tahoma,90,90,10,74,0 - 0 - wxID_ANY - version - - - m_staticTextVersion - public - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 4 - protected - 0 - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - Copyright © 2009 Satoshi Nakamoto. This is experimental software. Do not rely on it for actual financial transactions. Distributed under the MIT/X11 software license, see the accompanying file license.txt or http://www.opensource.org/licenses/mit-license.php. This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/) and cryptographic software written by Eric Young (eay@cryptsoft.com). - - - m_staticTextMain - protected - - - - - - - - - 400 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - - - 5 - wxALIGN_RIGHT|wxEXPAND - 0 - - - bSizer61 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CSendDialogBase - - 675,338 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Send Coins - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxEXPAND|wxLEFT - 0 - - 2 - wxBOTH - 1 - - 0 - - fgSizer1 - wxFLEX_GROWMODE_SPECIFIED - none - 3 - 0 - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Enter the recipient's IP address (e.g. 123.45.6.7) for online transfer with comments and confirmation, or bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L) if recipient is not online. - - - m_staticText14 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxLEFT - 1 - - 70,-1 - bSizer47 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - check; Load From Icon Resource [-1; -1] - - 1 - - - 0 - wxID_ANY - - - m_bitmapCheckMark - protected - - 16,16 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Pay &To: - - - m_staticText36 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxRIGHT - 1 - - - bSizer19 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 1 - - - - 1 - - - 0 - wxID_TEXTCTRLPAYTO - - 0 - - m_textCtrlAddress - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - OnTextAddress - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONPASTE - &Paste - - - m_buttonPaste - protected - - -1,-1 - wxBU_EXACTFIT - - - - - - OnButtonPaste - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONADDRESSBOOK - Address &Book... - - - m_buttonAddress - protected - - - - - - - - - OnButtonAddressBook - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT|wxALIGN_RIGHT - 0 - - - - 1 - - - 0 - wxID_ANY - &Amount: - - - m_staticText19 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - - 1 - - ,90,90,-1,70,0 - 0 - wxID_TEXTCTRLAMOUNT - - 20 - - m_textCtrlAmount - protected - - 145,-1 - - - - - - - - - - - OnKeyDown - - OnKillFocusAmount - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - T&ransfer: - - - m_staticText20 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - " Standard" - - 1 - - - 0 - wxID_CHOICETRANSFERTYPE - - - m_choiceTransferType - protected - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer672 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT - 1 - - - bSizer681 - wxVERTICAL - none - - 5 - wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - &From: - - - m_staticTextFrom - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxLEFT|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlFrom - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer67 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT - 1 - - - bSizer68 - wxVERTICAL - none - - 5 - wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - &Message: - - - m_staticTextMessage - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxLEFT - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlMessage - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer23 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - ,90,90,-1,70,0 - 0 - wxID_BUTTONSEND - &Send - - 85,25 - m_buttonSend - protected - - -1,-1 - - - - - - - OnButtonSend - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CSendingDialogBase - - 442,151 - wxDEFAULT_DIALOG_STYLE - - Sending... - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - OnPaint - - - - - - - - - bSizer68 - wxVERTICAL - none - - 8 - wxALIGN_CENTER_VERTICAL|wxEXPAND|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextSending - protected - - -1,14 - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 - wxEXPAND|wxRIGHT|wxLEFT - 1 - - wxSYS_COLOUR_BTNFACE - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlStatus - protected - - - wxTE_CENTRE|wxTE_MULTILINE|wxTE_NO_VSCROLL|wxTE_READONLY - - - Connecting... - - - wxNO_BORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 0 - - - 0 - wxID_ANY - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CYourAddressDialogBase - - 610,390 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Your Bitcoin Address - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer68 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - These are your Bitcoin addresses for receiving payments. You may want to give a different one to each sender so you can keep track of who is paying you. - - - m_staticText45 - protected - - - - - - - - - 600 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_LISTCTRL - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_ASCENDING - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListEndLabelEdit - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONRENAME - &Rename... - - 85,25 - m_buttonRename - protected - - - - - - - - - OnButtonRename - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEW - &New Address... - - 110,25 - m_buttonNew - protected - - -1,-1 - - - - - - - OnButtonNew - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCOPY - &Copy to Clipboard - - 120,25 - m_buttonCopy - protected - - -1,-1 - - - - - - - OnButtonCopy - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CAddressBookDialogBase - - 610,390 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Address Book - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer68 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 1 - wxID_ANY - Bitcoin Address - - - m_staticText55 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_LISTCTRL - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_ASCENDING - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListEndLabelEdit - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONEDIT - &Edit... - - 85,25 - m_buttonEdit - protected - - - - - - - - - OnButtonEdit - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEW - &New Address... - - 110,25 - m_buttonNew - protected - - - - - - - - - OnButtonNew - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONDELETE - &Delete - - 85,25 - m_buttonDelete - protected - - - - - - - - - OnButtonDelete - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - -1,-1 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - public - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CProductsDialogBase - - 708,535 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Marketplace - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer22 - wxVERTICAL - none - - 5 - wxEXPAND|wxTOP|wxBOTTOM|wxRIGHT - 0 - - - bSizer23 - wxHORIZONTAL - none - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - "(Any Category)" - - 1 - - - 0 - wxID_ANY - - - m_comboBoxCategory - protected - - 150,-1 - - - - (Any Category) - - - - - OnCombobox - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlSearch - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 0 - 1 - - - 0 - wxID_ANY - &Search - - - m_buttonSearch - protected - - - - - - - - - OnButtonSearch - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivated - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CEditProductDialogBase - - 660,640 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Edit Product - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - wxSYS_COLOUR_WINDOW - - 1 - - - 0 - wxID_ANY - - - m_scrolledWindow - protected - - 5 - 5 - - - - - - wxHSCROLL|wxTAB_TRAVERSAL|wxVSCROLL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND|wxTOP|wxRIGHT|wxLEFT - 0 - - 2 - wxBOTH - 1 - - 0 - - fgSizer8 - wxFLEX_GROWMODE_SPECIFIED - none - 0 - 0 - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Category - - - m_staticText106 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - - 1 - - - 0 - wxID_ANY - - 180,-1 - m_comboBoxCategory - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Title - - - m_staticText108 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlTitle - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Price - - - m_staticText107 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 105,-1 - m_textCtrlPrice - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Page 1: Description - - - m_staticText22 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - -1,170 - m_textCtrlDescription - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Page 2: Order Form - - - m_staticText23 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - -1,120 - m_textCtrlInstructions - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 3 - wxBOTH - 1 - - 0 - - fgSizer5 - wxFLEX_GROWMODE_SPECIFIED - public - 0 - 0 - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Label - - - m_staticText24 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Comma separated list of choices, or leave blank for text field - - - m_staticText25 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel0 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField0 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL - 0 - - - - 0 - 1 - - - 0 - wxID_DEL0 - Delete - - -1,-1 - m_buttonDel0 - protected - - 60,20 - - - - - - - OnButtonDel0 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel1 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField1 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL1 - Delete - - -1,-1 - m_buttonDel1 - protected - - 60,20 - - - - - - - OnButtonDel1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel2 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField2 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL2 - Delete - - -1,-1 - m_buttonDel2 - protected - - 60,20 - - - - - - - OnButtonDel2 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel3 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField3 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL3 - Delete - - -1,-1 - m_buttonDel3 - protected - - 60,20 - - - - - - - OnButtonDel3 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel4 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField4 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL4 - Delete - - -1,-1 - m_buttonDel4 - protected - - 60,20 - - - - - - - OnButtonDel4 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel5 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField5 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL5 - Delete - - -1,-1 - m_buttonDel5 - protected - - 60,20 - - - - - - - OnButtonDel5 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel6 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField6 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL6 - Delete - - -1,-1 - m_buttonDel6 - protected - - 60,20 - - - - - - - OnButtonDel6 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel7 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField7 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL7 - Delete - - -1,-1 - m_buttonDel7 - protected - - 60,20 - - - - - - - OnButtonDel7 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel8 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField8 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL8 - Delete - - -1,-1 - m_buttonDel8 - protected - - 60,20 - - - - - - - OnButtonDel8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel9 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField9 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL9 - Delete - - -1,-1 - m_buttonDel9 - protected - - 60,20 - - - - - - - OnButtonDel9 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel10 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField10 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL10 - Delete - - -1,-1 - m_buttonDel10 - protected - - 60,20 - - - - - - - OnButtonDel10 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel11 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField11 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL11 - Delete - - -1,-1 - m_buttonDel11 - protected - - 60,20 - - - - - - - OnButtonDel11 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel12 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField12 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL12 - Delete - - -1,-1 - m_buttonDel12 - protected - - 60,20 - - - - - - - OnButtonDel12 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel13 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField13 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL13 - Delete - - -1,-1 - m_buttonDel13 - protected - - 60,20 - - - - - - - OnButtonDel13 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel14 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField14 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL14 - Delete - - -1,-1 - m_buttonDel14 - protected - - 60,20 - - - - - - - OnButtonDel14 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel15 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField15 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL15 - Delete - - -1,-1 - m_buttonDel15 - protected - - 60,20 - - - - - - - OnButtonDel15 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel16 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField16 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL16 - Delete - - -1,-1 - m_buttonDel16 - protected - - 60,20 - - - - - - - OnButtonDel16 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel17 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField17 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL17 - Delete - - -1,-1 - m_buttonDel17 - protected - - 60,20 - - - - - - - OnButtonDel17 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel18 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField18 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL18 - Delete - - -1,-1 - m_buttonDel18 - protected - - 60,20 - - - - - - - OnButtonDel18 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel19 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField19 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL19 - Delete - - -1,-1 - m_buttonDel19 - protected - - 60,20 - - - - - - - OnButtonDel19 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_HORIZONTAL - 0 - - - bSizer25 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_ANY - &Add Field - - - m_buttonAddField - protected - - - - - - - - - OnButtonAddField - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONSEND - &Send - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonSend - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONPREVIEW - &Preview - - 85,25 - m_buttonPreview - protected - - - - - - - - - OnButtonPreview - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CViewProductDialogBase - - 630,520 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Order Form - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer116 - wxHORIZONTAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_htmlWinReviews - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxALL - 1 - - wxSYS_COLOUR_WINDOW - - 1 - - - 0 - wxID_ANY - - - m_scrolledWindow - protected - - 5 - 5 - - - - - - wxHSCROLL|wxTAB_TRAVERSAL|wxVSCROLL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - -1,-1 - m_richTextHeading - protected - - -1,50 - wxTE_READONLY - - - - - wxNO_BORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - Order Form instructions here multiple lines 1 2 3 4 5 6 - - - m_staticTextInstructions - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_HORIZONTAL - 0 - - - bSizer25 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONSAMPLE - &Submit - - -1,-1 - m_buttonSubmitForm - protected - - - - - - - - - OnButtonSubmitForm - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL2 - Cancel - - - m_buttonCancelForm - protected - - - - - - - - - OnButtonCancelForm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 0 - - - 0 - wxID_BUTTONBACK - < &Back - - 85,25 - m_buttonBack - protected - - - - - - - - - OnButtonBack - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEXT - &Next > - - 85,25 - m_buttonNext - protected - - - - - - - - - OnButtonNext - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CViewOrderDialogBase - - 630,520 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - View Order - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer116 - wxHORIZONTAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_htmlWin - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CEditReviewDialogBase - - 630,440 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Enter Review - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer112 - wxVERTICAL - none - - 5 - - 0 - - 3 - protected - 0 - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextSeller - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - - 0 - - 3 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Rating - - - m_staticText110 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - " 1 star" " 2 stars" " 3 stars" " 4 stars" " 5 stars" - - 1 - - - 0 - wxID_ANY - - - m_choiceStars - protected - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Review - - - m_staticText43 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlReview - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer113 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_SUBMIT - &Submit - - 85,25 - m_buttonSubmit - protected - - - - - - - - - OnButtonSubmit - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_BTNFACE - - - 1 - - - - 0 - wxID_ANY - - - CPokerLobbyDialogBase - - 586,457 - wxDEFAULT_FRAME_STYLE - - Poker Lobby - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer156 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - 130,-1 - m_treeCtrl - protected - - - wxTR_HAS_BUTTONS|wxTR_HIDE_ROOT|wxTR_LINES_AT_ROOT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnTreeSelChanged - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer172 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OPENNEWTABLE - &Open New Table - - - m_buttonNewTable - protected - - - - - - - - - OnButtonNewTable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CPokerDialogBase - - 806,550 - wxDEFAULT_FRAME_STYLE|wxFRAME_NO_TASKBAR - - Poker - - - - wxFULL_REPAINT_ON_RESIZE|wxTAB_TRAVERSAL - 1 - - - - OnClose - - - - - - - - - - - - - - - - - OnMouseEvents - - OnPaint - - - - - OnSize - - - - bSizer174 - wxVERTICAL - none - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - Deal Me Out - - - m_checkSitOut - public - - - - - - - - - - OnCheckSitOut - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_DEALHAND - &Deal Hand - - - m_buttonDealHand - protected - - 150,25 - - - - - - - OnButtonDealHand - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_FOLD - &Fold - - - m_buttonFold - protected - - 80,25 - - - - - - - OnButtonFold - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CALL - &Call - - - m_buttonCall - protected - - 80,25 - - - - - - - OnButtonCall - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_RAISE - &Raise - - - m_buttonRaise - protected - - 80,25 - - - - - - - OnButtonRaise - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_LEAVETABLE - &Leave Table - - - m_buttonLeaveTable - protected - - 90,25 - - - - - - - OnButtonLeaveTable - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_DITCHPLAYER - - 0 - - m_textDitchPlayer - protected - - 45,-1 - wxTE_PROCESS_ENTER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnDitchPlayer - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - FOLD - - - m_checkPreFold - public - - 100,-1 - - - - - - - - OnCheckPreFold - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - CALL - - - m_checkPreCall - public - - 100,-1 - - - - - - - - OnCheckPreCall - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - CALL ANY - - - m_checkPreCallAny - public - - 100,-1 - - - - - - - - OnCheckPreCallAny - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - RAISE - - - m_checkPreRaise - public - - 100,-1 - - - - - - - - OnCheckPreRaise - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - RAISE ANY - - - m_checkPreRaiseAny - public - - 100,-1 - - - - - - - - OnCheckPreRaiseAny - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - 1 - - 0 - wxID_ANY - - - m_statusBar - public - - - wxST_SIZEGRIP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CGetTextFromUserDialogBase - - 403,138 - wxDEFAULT_DIALOG_STYLE - - - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer79 - wxVERTICAL - none - - 10 - wxEXPAND|wxALL - 1 - - - bSizer81 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextMessage1 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL - 0 - - - - 1 - - - 0 - wxID_TEXTCTRL - - 0 - - m_textCtrl1 - protected - - - wxTE_PROCESS_ENTER - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 1 - wxID_ANY - - - - m_staticTextMessage2 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL - 0 - - - - 1 - - - 1 - wxID_TEXTCTRL - - 0 - - m_textCtrl2 - protected - - - wxTE_PROCESS_ENTER - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 0 - - - bSizer80 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - -1,-1 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/util.cpp b/src/util.cpp index 7f56530..68bd18e 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -8,6 +8,99 @@ bool fDebug = false; +// --------------------------------------------------------------------------- +// Categorized, burst-collapsing logging -- see the LogPrint doc comment in +// util.h for the problem this solves. +// --------------------------------------------------------------------------- + +// Categories that explain the pool/worker/payout pipeline are on unconditionally: +// silence there is exactly the "worker stuck, nothing then happens" complaint +// this exists to fix, so it can't be opted out of by forgetting -debug. +// "net" and "nostr" cover the per-address connection/discovery chatter that +// used to dominate debug.log; they're gated behind fDebug like the printf() +// calls they replace, so default output stays quiet unless requested. +static bool LogCategoryDefaultOn(const char* category) +{ + return strcmp(category, "pool") == 0 || + strcmp(category, "worker") == 0 || + strcmp(category, "payout") == 0; +} + +bool LogAcceptsCategory(const char* category) +{ + if (LogCategoryDefaultOn(category)) + return true; + return fDebug; +} + +// Only collapse bursts from the exact same call site within this window; +// a different LogPrint() call (different file/line) is never merged with +// this one, so unrelated messages never get mixed into one summary. +static const int64 DEDUP_WINDOW_SECS = 3; + +struct LogDedupState { + int64 windowStart; // GetTime() when the current window opened + int suppressed; // calls swallowed so far this window + std::string lastMsg; // most recent message text, for the summary line +}; + +static std::map g_logDedup; +static CCriticalSection cs_logDedup; + +void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) +{ + // Call sites follow the existing printf() convention of ending their + // format string with \n; strip it here so the single \n this function + // adds below doesn't turn into a blank line. + std::string msg = msgIn; + while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) + msg.pop_back(); + + char key[512]; + my_snprintf(key, sizeof(key), "%s|%s:%d", category, file, line); + + int64 now = GetTime(); + bool fEmitNow = false; + int suppressedToFlush = 0; + std::string flushedMsg; + + CRITICAL_BLOCK(cs_logDedup) + { + std::map::iterator it = g_logDedup.find(key); + if (it == g_logDedup.end()) + { + // First time this call site has fired (or its last window is long + // gone) -- print it directly and open a fresh window. + g_logDedup[key] = LogDedupState{now, 0, msg}; + fEmitNow = true; + } + else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) + { + // Window rolled over: flush what was suppressed during it (if + // anything), then start a new window with this call printed. + suppressedToFlush = it->second.suppressed; + flushedMsg = it->second.lastMsg; + it->second.windowStart = now; + it->second.suppressed = 0; + it->second.lastMsg = msg; + fEmitNow = true; + } + else + { + // Still inside the window and already printed one line for it: + // count this occurrence instead of writing another near-duplicate. + it->second.suppressed++; + it->second.lastMsg = msg; + } + } + + if (suppressedToFlush > 0) + OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", + category, suppressedToFlush, (long long)DEDUP_WINDOW_SECS, flushedMsg.c_str()); + if (fEmitNow) + OutputDebugStringF("[%s] %s\n", category, msg.c_str()); +} + @@ -200,11 +293,16 @@ void PrintException(std::exception* pex, const char* pszThread) snprintf(pszMessage, sizeof(pszMessage), "UNKNOWN EXCEPTION \n%s in %s \n", pszModule, pszThread); printf("\n\n************************\n%s", pszMessage); - // Suppress the dialog and rethrow during shutdown — threads unwinding on - // exit cause spurious "UNKNOWN EXCEPTION" popups that aren't real errors. - // Show error in GUI via repaint signal — GUI polls g_errorMessage - if (!fShutdown) - throw; + // Do NOT rethrow here. Every caller of PrintException (via + // CATCH_PRINT_EXCEPTION) is a thread wrapped in its own + // "try { ThreadFoo2() } catch(...) { log, sleep, loop again }" self-healing + // retry loop -- that's the whole point of the wrapper. Rethrowing from + // inside that catch block escapes the retry loop entirely (nothing above + // it in the thread catches it) and takes down the whole process with + // std::terminate() on the very first transient error -- a flaky relay + // connection, a malformed message, anything -- instead of letting the + // thread log it, sleep 5s, and try again like it's designed to. + // Show error in GUI via repaint signal -- GUI polls g_errorMessage } diff --git a/src/util.h b/src/util.h index c53293a..641b838 100644 --- a/src/util.h +++ b/src/util.h @@ -26,11 +26,14 @@ typedef unsigned long long uint64; #define UEND(a) ((unsigned char*)&((&(a))[1])) #define ARRAYLEN(array) (sizeof(array)/sizeof((array)[0])) -// Route printf to debug.log (and the console) on the GUI Windows build and on -// the headless Linux build alike. -#if defined(_WINDOWS) || !defined(_WIN32) +// Route printf to debug.log (and the console on non-Windows) everywhere. +// (Previously gated on `defined(_WINDOWS) || !defined(_WIN32)` -- but +// _WINDOWS is only ever defined by the legacy, unused makefile.vc build; +// the real Windows build (makefile.mingw) never defines it, so on actual +// Windows this condition was always false and debug.log was never written. +// OutputDebugStringF() is already platform-safe internally: it always +// writes debug.log, and only echoes to the console when not on Windows.) #define printf OutputDebugStringF -#endif #ifdef snprintf #undef snprintf @@ -70,6 +73,35 @@ inline T& REF(const T& val) extern bool fDebug; +// --------------------------------------------------------------------------- +// Categorized, burst-collapsing logging +// +// Plain printf() (routed to OutputDebugStringF below) writes every call +// unfiltered to debug.log. That's fine for one-off events, but a loop that +// logs once per item -- once per discovered peer, once per connect attempt, +// once per share -- turns debug.log into a wall of near-identical lines that +// differ only by an address, while the line that actually explains a stuck +// flow ("Stratum: no job available for ...") scrolls off screen under the +// noise. LogPrint() is a drop-in replacement for printf() that fixes both +// problems without touching call sites' formatting: +// +// 1. Category tag: every call names the subsystem it belongs to ("pool", +// "worker", "payout", "net", "nostr", ...). LogAcceptsCategory() +// controls what's written (see util.cpp); the pool/worker/payout flow +// the person actually needs visibility into is on by default. +// 2. Burst collapse: repeated LogPrint() calls from the *same source line* +// only emit one line per DEDUP_WINDOW_SECS; if more calls land inside +// that window they're counted instead of reprinted, and the count is +// flushed as a single "(xN similar in Ws)" line the moment the window +// rolls over. A loop discovering 500 peers by address now produces a +// small, bounded number of lines instead of 500 -- log growth is capped +// by elapsed time, not by how many similar events happen to fire. +// --------------------------------------------------------------------------- +bool LogAcceptsCategory(const char* category); +void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); +#define LogPrint(category, ...) \ + do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) + void RandAddSeed(bool fPerfmon=false); int my_snprintf(char* buffer, size_t limit, const char* format, ...); string strprintf(const char* format, ...); @@ -231,7 +263,7 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp -// Forward declaration — defined in main.cpp +// Forward declaration -- defined in main.cpp string GetAppDir(); inline int OutputDebugStringF(const char* pszFormat, ...) -- cgit v1.2.3 From c404e1c7f5d3f04d07e7e5b35a7a8ffcc998d855 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Wed, 29 Jul 2026 16:33:02 -0300 Subject: build: headless Linux node that does not need libGL (#37) The GUI build requires libGL at load time even when started with -nogui. The dynamic loader resolves every dependency before main() runs, so the flag arrives far too late -- on a clean server the binary dies with error while loading shared libraries: libGL.so.1: cannot open shared object file and nothing suggests it would otherwise have worked. A server is where a node most naturally lives, so this is the deployment that mattered most and the one that failed. The AppImage additionally needs FUSE, which minimal servers lack. Bundling libGL into the AppImage would be the wrong fix: GL has to come from the host's driver stack, and shipping our own breaks machines that have a real GPU. So: a second binary, built from the same node objects without ImGui, GLFW or OpenGL. `make linux-node` produces bitflash-node--x86_64, 2.5 MB stripped, with no graphics or FUSE dependency at all. `make linux` now builds it alongside the AppImage. Anyone wanting a window keeps using the AppImage. Two small moves were needed. MainFrameRepaint is called from main.cpp and net.cpp but defined in gui.cpp, so headless.cpp provides an empty one rather than putting ifdefs at six call sites. DateTimeStr also lived in gui.cpp while main.cpp logs block times with it -- nothing about formatting a date is GUI work, so it moved to util.cpp and both builds share one definition. Verified on Debian 13: ldd bitflash-node | grep -iE 'libGL|glfw|X11' -> nothing ldd bitflash | grep -iE 'libGL|glfw' -> libglfw.so.3, libGL.so.1, libGLdispatch.so.0 and at runtime, from an empty datadir: btfseed: reached ygffz37...btf via 90.156.222.107:8434 rendezvous: registered at 90.156.222.107:8434, waiting for a dial self-resolve OK (meeting_node=90.156.222.107:8434) btfpeers: kept 15 of 15 descriptor(s) first block at ~30s, 3182 blocks accepted, synced to the tip The GUI builds on Linux and Windows were rebuilt and are unaffected. --- Makefile | 12 ++++++++++++ src/Makefile | 32 +++++++++++++++++++++++++++++++- src/gui.cpp | 10 ++-------- src/headless.cpp | 20 ++++++++++++++++++++ src/main_gui.cpp | 13 +++++++++++++ src/util.cpp | 12 ++++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 src/headless.cpp (limited to 'src/gui.cpp') diff --git a/Makefile b/Makefile index 9cfef2f..b5bfa9a 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,18 @@ linux: deps-linux $(MAKE) -C src -f Makefile -j$(NPROC) strip src/bitflash $(MAKE) -f Makefile appimage + $(MAKE) -f Makefile linux-node + +# Headless node, no ImGui/GLFW/OpenGL. The GUI build needs libGL present at load +# time even when started with -nogui -- the dynamic loader resolves everything +# before main() runs, so the flag comes far too late. On a clean server, which is +# where a node most naturally lives, that is a hard failure. This binary has no +# such dependency and needs no FUSE either, so it drops onto a VPS and runs. +linux-node: deps-linux + $(MAKE) -C src -f Makefile bitflash-node -j$(NPROC) + strip src/bitflash-node + cp src/bitflash-node bitflash-node-$(VERSION)-x86_64 + @echo "Built: bitflash-node-$(VERSION)-x86_64" deps-linux: deps-apt deps-secp256k1 deps-randomx diff --git a/src/Makefile b/src/Makefile index c0b6e1d..9846d8e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -71,5 +71,35 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS) @echo "Built: bitflash" +# ---- headless node --------------------------------------------------------- +# +# Same node, built without ImGui, GLFW or OpenGL. Linking the GUI made the +# binary require libGL at load time even when started with -nogui, because the +# dynamic loader resolves everything before main() runs -- so on a clean server, +# which is where a node most naturally lives, it failed to start with an error +# about shared libraries and no hint that it would otherwise work. +# +# Objects go to obj-node/ because main_gui.cpp is compiled twice, with and +# without -DBITFLASH_NO_GUI, and the two must not overwrite each other. + +NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ + -L$(RANDOMX_DIR)/build -lrandomx \ + -lsodium -lssl -lcrypto -ldb_cxx -lpthread + +NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ + obj-node/main_gui.o obj-node/headless.o + +obj-node/%.o: %.cpp $(HEADERS) + @mkdir -p obj-node + $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< + +obj-node/sha.o: sha.cpp sha.h + @mkdir -p obj-node + $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -O3 -o $@ $< + +bitflash-node: $(NODE_ONLY_OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) + @echo "Built: bitflash-node (headless, no libGL needed)" + clean: - rm -rf obj bitflash + rm -rf obj obj-node bitflash bitflash-node diff --git a/src/gui.cpp b/src/gui.cpp index a1a0698..fc6f1e1 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -171,14 +171,8 @@ static void RefreshWallet() void MainFrameRepaint() { g_needRefresh = true; } -string DateTimeStr(int64 nTime) -{ - time_t t = (time_t)nTime; - struct tm* p = localtime(&t); - if (!p) return ""; - char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); - return buf; -} +// DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is +// needed by builds that have no GUI at all. // --------------------------------------------------------------------------- // Status bar -- separate fullscreen-width overlay pinned to bottom edge. diff --git a/src/headless.cpp b/src/headless.cpp new file mode 100644 index 0000000..3372936 --- /dev/null +++ b/src/headless.cpp @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Stand-ins for the handful of symbols the node core expects the GUI to +// provide, so a headless binary can be linked without ImGui, GLFW or OpenGL. +// +// This exists because linking the GUI made the node need libGL at load time +// even when started with -nogui: the dynamic loader resolves everything before +// main() runs, so the flag came far too late. On a clean server -- which is +// where a node most naturally lives -- that is a hard failure with an error +// message about shared libraries and no hint that the program would otherwise +// have worked fine. + +#include "headers_core.h" + +// The GUI uses this to mark itself dirty. With no window to repaint there is +// nothing to do, and the call sites in main.cpp and net.cpp stay unchanged. +void MainFrameRepaint() +{ +} diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 9f7e34e..99e63d4 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -5,7 +5,11 @@ #include #endif +// BITFLASH_NO_GUI builds the same entry point without ImGui, GLFW or OpenGL, +// so the binary does not need libGL present just to start. See headless.cpp. +#ifndef BITFLASH_NO_GUI int RunGUI(int argc, char* argv[]); +#endif // Global definitions (were in ui.cpp, now here) map mapAddressBook; @@ -186,8 +190,13 @@ int main(int argc, char* argv[]) if (_beginthread(ThreadBitcoinMiner, 0, NULL) == (uintptr_t)-1) printf("Error: _beginthread(ThreadBitcoinMiner) failed\n"); +#ifdef BITFLASH_NO_GUI + // Nothing else this binary can do; /nogui is accepted and redundant. + bool fHeadless = true; +#else bool fHeadless = arg(argc,argv,"/nogui") || arg(argc,argv,"-nogui") || arg(argc,argv,"/daemon") || arg(argc,argv,"-daemon"); +#endif if (fHeadless) { #ifndef _WIN32 @@ -200,8 +209,12 @@ int main(int argc, char* argv[]) return 0; } +#ifdef BITFLASH_NO_GUI + return 0; // unreachable: fHeadless is always true in this build +#else int ret = RunGUI(argc, argv); fShutdown = true; StopNode(); return ret; +#endif } diff --git a/src/util.cpp b/src/util.cpp index 68bd18e..e4e314f 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -490,3 +490,15 @@ void AddTimeData(unsigned int ip, int64 nTime) printf("| nTimeOffset = %+lld (%+lld minutes)\n", nTimeOffset, nTimeOffset/60); } } + +// Formats a block or transaction timestamp for logs and for the wallet view. +// It lived in gui.cpp, but main.cpp logs block times with it, so a headless +// build needed it too -- and nothing about formatting a date is GUI work. +string DateTimeStr(int64 nTime) +{ + time_t t = (time_t)nTime; + struct tm* p = localtime(&t); + if (!p) return ""; + char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); + return buf; +} -- cgit v1.2.3 From 225d0210f881acb10a3dfaf9bc5a23446bc12d21 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 30 Jul 2026 14:07:50 -0300 Subject: mine: hash on every core instead of one (#52) Satoshi left a note above ThreadBitcoinMiner in 2009: //// todo: start one thread per processor, //// use getenv("NUMBER_OF_PROCESSORS") Nobody did, and it stopped being a nicety when the proof of work became RandomX. SHA-256d on a 2009 desktop lost little to a single thread. RandomX is built to be fed by every core at once, so one thread on a 32-core machine mines at a thirty-second of what the box can do -- and the only outward sign is a CPU that looks suspiciously idle. A user reported exactly that: 30 minutes of "Mining", no blocks, Bitflash sitting at 3.3% CPU while the 2 GB dataset was allocated and fast mode was active. Nothing was broken. It was hashing on one core. bitflash -gen # every core but one bitflash -gen -genproclimit=8 # exactly eight Automatic leaves one core to the rest of the node -- accepting blocks, talking to peers, drawing the window -- because a node that mines beautifully while falling behind the chain has won nothing. /genproclimit overrides it, and a count several times the core count is refused rather than quietly thrashing. The wrapper was already built for this: RandomXCreateMinerVM says "own VM, not shared", and the ~2 GB dataset is one allocation every thread reads. Only the 2 MB scratchpad multiplies, so sixteen threads cost about 32 MB more than one. The startup line says so, because "why is it using 2 GB" is the first question anyone asks. Two things had to be right for this to be safe: - Shutdown. Miners share slot 3 of vfThreadRunning and StopNode waits for every slot before DBFlush tears the database down. With several, the first one out announced that everyone had finished while the rest were still calling ProcessBlock, so they are counted now and only the last one clears the flag. The three CheckForShutdown(3) calls inside the miner became plain returns: _endthread() does not unwind on Windows, so the bookkeeping would have been skipped there. - Duplicated work. Each thread makes its own key, and the coinbase pays it, so every thread has a different merkle root and searches a different space. Block assembly was already under cs_main and cs_mapTransactions; AddKey and ProcessBlock take their own locks. Verified on both platforms: four distinct thread ids reach the hashing loop and all report back on shutdown, which exits 0 with DBFlush running. Auto picks cores-1 (1 on a 2-core box, correctly reported). Speedup itself needs a many-core machine to show and has not been measured here -- the claim in this message is about threads doing work, not a benchmark. Not touched: RandomX's memory. RANDOMX_DATASET_BASE_SIZE and SCRATCHPAD_L3 are part of the hash function, so a knob for "use more RAM" would either do nothing or fork the chain. Threads are the only real dial. --- src/gui.cpp | 3 ++- src/main.cpp | 51 +++++++++++++++++++++++++++++++++++++++++++------ src/main.h | 8 +++++++- src/main_gui.cpp | 25 ++++++++++++++++++++++-- src/net.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++------ src/net.h | 2 ++ 6 files changed, 131 insertions(+), 16 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index fc6f1e1..825202b 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -32,6 +32,7 @@ int64 GetBalance(); string PubKeyToAddress(const std::vector& vchPubKey); bool AddressToHash160(const std::string& str, uint160& hash160Ret); void ThreadBitcoinMiner(void*); +int StartMinerThreads(); void ThreadRPCServer(void*); // --------------------------------------------------------------------------- @@ -762,7 +763,7 @@ static void DrawOptionsDialog() fGenerateBitcoins = 0; } else { fGenerateBitcoins = 1; - if (!vfThreadRunning[3]) _beginthread(ThreadBitcoinMiner, 0, NULL); + if (!vfThreadRunning[3]) StartMinerThreads(); } if (nMineMode == MINE_OPERATOR) { diff --git a/src/main.cpp b/src/main.cpp index c882b0b..d4fc6d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,6 +72,13 @@ bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local t // Settings int fGenerateBitcoins; +// How many threads to hash with. 0 means "decide from the hardware" -- see +// MinerThreadCount(). This was effectively 1 forever, because Bitcoin 0.1.0 +// started exactly one miner thread and nothing here ever changed that. It made +// sense for SHA-256d in 2009; RandomX is built to be fed by every core at once, +// so a 32-core machine was mining at a thirty-second of its capacity and the +// only visible symptom was a suspiciously idle CPU. +int nMinerThreads = 0; int64 nTransactionFee = 0; CAddress addrIncoming; int nMineMode = MINE_RELAY; @@ -119,6 +126,26 @@ void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, doubl // mapKeys // +// Threads to hash with, resolving the automatic case. +// +// Automatic leaves one core to the rest of the node -- accepting blocks, +// talking to peers, serving the GUI -- because a machine that mines perfectly +// while falling behind the chain has won nothing. Anyone who disagrees can say +// so with /genproclimit and get exactly what they ask for. +// +// Memory is not the reason to hold back: the ~2 GB RandomX dataset is shared by +// every thread, and each one adds only its own 2 MB scratchpad. Sixteen threads +// cost about 32 MB more than one. +int MinerThreadCount() +{ + if (nMinerThreads > 0) + return nMinerThreads; + unsigned int nCores = std::thread::hardware_concurrency(); + if (nCores <= 1) + return 1; + return (int)(nCores - 1); +} + bool AddKey(const CKey& key) { CRITICAL_BLOCK(cs_mapKeys) @@ -2769,7 +2796,7 @@ static bool PoolParticipantMiner() return true; } -bool BitcoinMiner() +bool BitcoinMiner(int nThreadId) { // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here // since the GUI hides Start Mining for Relay, but guard it regardless.) @@ -2809,11 +2836,17 @@ bool BitcoinMiner() while (fGenerateBitcoins) { Sleep(50); - CheckForShutdown(3); + // Return instead of _endthread()ing. With more than one miner thread the + // bookkeeping in ThreadBitcoinMiner has to run on the way out, and + // _endthread() does not unwind on Windows, so a normal return is the + // only exit that works the same on both platforms. + if (fShutdown) + return true; while (vNodes.empty() && !fSoloMineTest) { Sleep(1000); - CheckForShutdown(3); + if (fShutdown) + return true; } unsigned int nTransactionsUpdatedLast = nTransactionsUpdated; @@ -2908,8 +2941,11 @@ bool BitcoinMiner() Sleep(1000); continue; } - LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n", - RandomXFastReady() ? "fast 2GB" : "light 256MB"); + // The thread number is in the line on purpose: every miner would + // otherwise print the same text, and the dedup filter would fold them + // into one, leaving no way to tell four threads from one. + LogPrint("net", "BitcoinMiner: thread %d hashing with RandomX (%s)\n", + nThreadId, RandomXFastReady() ? "fast 2GB" : "light 256MB"); unsigned int nStart = GetTime(); uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256(); @@ -2950,7 +2986,8 @@ bool BitcoinMiner() // ~256 hashes is already plenty of time) if ((++pblock->nNonce & 0xff) == 0) { - CheckForShutdown(3); + if (fShutdown) + break; // falls through to RandomXDestroyMinerVM below if (pblock->nNonce == 0) break; if (pindexPrev != pindexBest) @@ -2963,6 +3000,8 @@ bool BitcoinMiner() } } RandomXDestroyMinerVM(rxvm); + if (fShutdown) + break; } return true; diff --git a/src/main.h b/src/main.h index 7e5bdf2..750d3f1 100644 --- a/src/main.h +++ b/src/main.h @@ -73,6 +73,10 @@ extern string strPoolName; // operator: announced pool name extern string strPoolDashboardUrl; // operator: optional dashboard URL extern double dPoolFeePercent; // operator: announced fee percent extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test) +// Threads to hash with; 0 = decide from the hardware. Set by /genproclimit. +extern int nMinerThreads; +// Resolves the automatic case into a real count. +int MinerThreadCount(); extern bool gPoolServerRunning; // GUI/startup state extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp) @@ -117,7 +121,9 @@ void PrintBlockTree(); void AddOrphanTx(const CDataStream& vMsg); void EraseOrphanTx(uint256 hash); void LimitOrphanTx(unsigned int nMaxOrphans); -bool BitcoinMiner(); +// nThreadId is 1..N, only for logging: it makes each miner's lines +// distinguishable, which the dedup filter would otherwise collapse into one. +bool BitcoinMiner(int nThreadId = 1); void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); void SetParticipantMiningStatus(const std::string& status); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 2484592..852ad4d 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -1,6 +1,7 @@ // Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" +#include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 #include #endif @@ -52,6 +53,7 @@ static void PrintUsage() printf(" /operator\n"); printf(" /participant=POOL_BTF_ADDRESS\n"); printf(" /solomine\n"); + printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n"); printf("\n"); printf("Pool operator announcement:\n"); printf(" /poolname=NAME\n"); @@ -99,6 +101,26 @@ static void ParseStartupArguments(int argc, char* argv[]) if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine")) fSoloMineTest = true; + // /genproclimit=N -- threads to hash with. 0 or absent means automatic, + // which is every core but one. Named after Bitcoin's own option so it reads + // familiarly to anyone who has run one of these before. + string strProcLimit = argval2(argc, argv, "/genproclimit", "-genproclimit"); + if (!strProcLimit.empty()) + { + int n = atoi(strProcLimit.c_str()); + unsigned int nCores = std::thread::hardware_concurrency(); + if (n < 0) + fprintf(stderr, "Ignoring /genproclimit=%s: not a thread count\n", + strProcLimit.c_str()); + else if (nCores > 0 && n > (int)nCores * 4) + // Well past diminishing returns, and every thread still costs a + // scratchpad. Refuse rather than quietly thrash the machine. + fprintf(stderr, "Ignoring /genproclimit=%d: this machine has %u core(s)\n", + n, nCores); + else + nMinerThreads = n; // 0 stays automatic + } + if (arg(argc,argv,"/operator") || arg(argc,argv,"-operator")) nMineMode = MINE_OPERATOR; @@ -289,8 +311,7 @@ int main(int argc, char* argv[]) printf("Error: _beginthread(ThreadRPCServer) failed\n"); } if (fGenerateBitcoins) - if (_beginthread(ThreadBitcoinMiner, 0, NULL) == (uintptr_t)-1) - printf("Error: _beginthread(ThreadBitcoinMiner) failed\n"); + StartMinerThreads(); #ifdef BITFLASH_NO_GUI // Nothing else this binary can do; /nogui is accepted and redundant. diff --git a/src/net.cpp b/src/net.cpp index 2581578..9ee056f 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -10,6 +10,7 @@ #undef snprintf #endif #include "headers.h" +#include // hardware_concurrency, for the miner thread count #ifdef _WIN32 #include #endif @@ -1224,18 +1225,63 @@ void ThreadMessageHandler2(void* parg) -//// todo: start one thread per processor, use getenv("NUMBER_OF_PROCESSORS") +// Satoshi's "todo: start one thread per processor" stood here since 2009 and is +// what MinerThreadCount() and StartMinerThreads() below finally do -- by way of +// std::thread::hardware_concurrency rather than NUMBER_OF_PROCESSORS, which only +// ever existed on Windows. +// +// Miners share slot 3 of vfThreadRunning, and StopNode waits for every slot to +// go false before the node tears the database down. With one miner that was the +// same thing as "the miner finished"; with several, the first one out would +// announce that everybody had finished while the rest were still hashing and +// still calling ProcessBlock. So count them, and let only the last one leaving +// clear the flag. +static CCriticalSection cs_nMinersRunning; +static int nMinersRunning = 0; + void ThreadBitcoinMiner(void* parg) { - vfThreadRunning[3] = true; - CheckForShutdown(3); + int nThreadId = (int)(intptr_t)parg; + CRITICAL_BLOCK(cs_nMinersRunning) + { + nMinersRunning++; + vfThreadRunning[3] = true; + } try { - bool fRet = BitcoinMiner(); - printf("BitcoinMiner returned %s\n\n\n", fRet ? "true" : "false"); + bool fRet = BitcoinMiner(nThreadId); + printf("BitcoinMiner thread %d returned %s\n", nThreadId, fRet ? "true" : "false"); } CATCH_PRINT_EXCEPTION("BitcoinMiner()") - vfThreadRunning[3] = false; + CRITICAL_BLOCK(cs_nMinersRunning) + { + if (--nMinersRunning <= 0) + { + nMinersRunning = 0; + vfThreadRunning[3] = false; + } + } +} + +// Start the miners. Returns how many actually got off the ground. +int StartMinerThreads() +{ + int nThreads = MinerThreadCount(); + int nStarted = 0; + for (int i = 0; i < nThreads; i++) + { + if (_beginthread(ThreadBitcoinMiner, 0, (void*)(intptr_t)(i + 1)) == (uintptr_t)-1) + printf("Error: _beginthread(ThreadBitcoinMiner) failed on thread %d\n", i + 1); + else + nStarted++; + } + // The dataset is one allocation shared by all of them; only the 2 MB + // scratchpads multiply. Printed because "why is it using 2 GB" is the first + // question anyone asks. + printf("Mining with %d thread(s) of %u core(s) -- about %d MB " + "(~2080 MB shared RandomX dataset + ~2 MB per thread)\n", + nStarted, std::thread::hardware_concurrency(), 2080 + 2 * nStarted); + return nStarted; } diff --git a/src/net.h b/src/net.h index 4379d04..11c023e 100644 --- a/src/net.h +++ b/src/net.h @@ -58,6 +58,8 @@ int BtfPexAccept(const std::vector& vDesc); void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); bool AnySubscribed(unsigned int nChannel); void ThreadBitcoinMiner(void* parg); +// Starts MinerThreadCount() miners; returns how many came up. +int StartMinerThreads(); bool StartNode(string& strError=REF(string())); bool StopNode(); void CheckForShutdown(int n); -- cgit v1.2.3 From 86f080cc6deb0464edf3913daa2e1131b5eed7c7 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 30 Jul 2026 18:22:51 -0300 Subject: net: announce our height in the handshake, and say when we are behind (#56) A node running this code cannot tell "in sync with everyone" from "not hearing anyone". The version message carries no height -- 0.1.0 never had the field and this fork inherited the gap -- so the only evidence of a problem is a number on screen that stopped moving, which looks the same as a network with nothing to report. It is the structural reason a broken node here has always been indistinguishable from an idle one. Append nBestHeight to the version message and keep what a peer announced in CNode::nStartingHeight. The field goes last, so a node that predates it stops reading after addr and treats the rest as trailing bytes, which cost it one log line. Reading it is guarded by vRecv.empty() and never required: peers that do not send it keep nStartingHeight at -1 and are left out of the median. Every release so far sends the short form. GetPeerMedianHeight() takes the median rather than the maximum so a single peer claiming an absurd height cannot move it. Then use it. After five minutes more than one block behind the peer median, the node says so in the log -- not behind a debug category, because an operator who already enabled -debug is not the one who needs telling -- and the GUI carries the same sentence in its status area. "8 blocks behind for 27 minutes" is something a user can act on; a frozen counter is not. Verified against two nodes on the live network: the one starting from an empty datadir read its peer at 3797, reported 2636 then 1274 then 433 blocks behind as it caught up, and went quiet on its own once level. The other logged "peer height = -1" for the older nodes it met on the real network, which is the compatibility path doing its job. Known limit: nStartingHeight is a snapshot from handshake time and goes stale on a long-lived connection, so this catches a node that is receiving something but not blocks, not one that has gone completely deaf. The inactivity timeout from #55 covers that case and refreshes these heights when it forces the reconnect. --- src/gui.cpp | 10 ++++++++++ src/main.cpp | 8 +++++++- src/net.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/net.h | 42 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 119 insertions(+), 2 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 825202b..28f5ffa 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -26,6 +26,7 @@ extern CAddress addrLocalHost; void MainFrameRepaint(); int GetDiscoveredPeerCount(); +int GetPeerMedianHeight(); string DateTimeStr(int64 nTime); bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); int64 GetBalance(); @@ -238,6 +239,15 @@ static void DrawStatusBar() connected, discovered, nBestHeight); } + // A node that has stopped receiving looks exactly like one with nothing to + // do. Now that peers announce their height, say the difference out loud + // instead of leaving the user to guess from a number that stopped moving. + int peerHeight = GetPeerMedianHeight(); + if (peerHeight >= 0 && nBestHeight < peerHeight - 1) + ImGui::TextColored(ImVec4(0.95f, 0.65f, 0.20f, 1.0f), + "Behind the network by %d block(s) -- peers report height %d", + peerHeight - nBestHeight, peerHeight); + ImGui::End(); } diff --git a/src/main.cpp b/src/main.cpp index c77448d..15805ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1884,6 +1884,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (pfrom->nVersion == 0) return false; + // Optional: peers older than this field simply end the message here, + // and nStartingHeight stays -1 for them. Never make this a requirement + // -- every node released so far sends the short form. + if (!vRecv.empty()) + vRecv >> pfrom->nStartingHeight; + pfrom->vSend.SetVersion(min(pfrom->nVersion, VERSION)); pfrom->vRecv.SetVersion(min(pfrom->nVersion, VERSION)); @@ -1912,7 +1918,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (!vPexOut.empty()) pfrom->PushMessage("btfpeers", vPexOut); - if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("version addrMe = %s, peer height = %d\n", addrMe.ToString().c_str(), pfrom->nStartingHeight); } diff --git a/src/net.cpp b/src/net.cpp index 81932d7..ac8df82 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1225,6 +1225,65 @@ void ThreadMessageHandler(void* parg) } } +int GetPeerMedianHeight() +{ + vector vHeights; + CRITICAL_BLOCK(cs_vNodes) + foreach(CNode* pnode, vNodes) + if (pnode->nStartingHeight >= 0) + vHeights.push_back(pnode->nStartingHeight); + + if (vHeights.empty()) + return -1; + + sort(vHeights.begin(), vHeights.end()); + return vHeights[vHeights.size() / 2]; +} + + +// Say out loud when we are behind the network. +// +// This is the whole reason the height is in the handshake. Every serious bug +// this node has had looked identical from the outside: running, threads alive, +// nothing in the log, and no blocks arriving. A node that knows where everyone +// else is can say so, and "8 blocks behind for 27 minutes" is a sentence a +// user can act on. Deliberately not behind a debug category -- an operator who +// already suspects trouble is not the one who needs telling. +static void WarnIfBehind() +{ + static int64 nLastWarned; + static int64 nBehindSince; + + int nPeers = GetPeerMedianHeight(); + if (nPeers < 0 || nBestHeight < 0) + return; + + // One block of slack: somebody is always mid-relay. + if (nBestHeight >= nPeers - 1) + { + nBehindSince = 0; + return; + } + + int64 nNow = GetTime(); + if (nBehindSince == 0) + { + nBehindSince = nNow; + return; + } + + // Falling briefly behind is ordinary. Staying behind is not. + if (nNow - nBehindSince < BTF_BEHIND_GRACE_SECS) + return; + if (nNow - nLastWarned < BTF_BEHIND_WARN_INTERVAL_SECS) + return; + + nLastWarned = nNow; + printf("WARNING: %d blocks behind the network (height %d, peers report %d) for %d minutes\n", + nPeers - nBestHeight, nBestHeight, nPeers, (int)((nNow - nBehindSince) / 60)); +} + + void ThreadMessageHandler2(void* parg) { printf("ThreadMessageHandler started\n"); @@ -1250,6 +1309,8 @@ void ThreadMessageHandler2(void* parg) pnode->Release(); } + WarnIfBehind(); + // Wait and allow messages to bunch up vfThreadRunning[2] = false; Sleep(100); diff --git a/src/net.h b/src/net.h index 449cff5..831a880 100644 --- a/src/net.h +++ b/src/net.h @@ -69,6 +69,21 @@ static const int BTF_HANDSHAKE_GRACE_SECS = 60; // stay well under BTF_RECV_TIMEOUT_SECS. static const int BTF_PING_INTERVAL_SECS = 10 * 60; +// How long a node must stay behind the network before it says so, and how +// often it repeats itself. Five minutes is two and a half block intervals -- +// long enough that ordinary relay lag never trips it. +static const int BTF_BEHIND_GRACE_SECS = 5 * 60; +static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60; + +// Defined in main.cpp. Declared here because CNode announces it in the version +// message, and net.h is included before main.h. +extern int nBestHeight; + +// Median height announced by the peers we are connected to, or -1 while nobody +// has told us anything. Median rather than maximum so one peer claiming an +// absurd height cannot move it. +int GetPeerMedianHeight(); + // Descriptors to hand a peer: ours first, then peers that answered us. void BtfPexCollect(std::vector& vDescOut); // Verify descriptors a peer sent and remember the good ones. Returns how many. @@ -528,6 +543,11 @@ public: int64 nLastRecv; int64 nLastSendEmpty; + // Height this peer announced in its version message, or -1 if it sent a + // version message that predates the field. Knowing how far along everyone + // else is, is the only way a node can tell "in sync" from "not hearing". + int nStartingHeight; + CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) { @@ -549,12 +569,16 @@ public: nLastSend = 0; nLastRecv = 0; nLastSendEmpty = GetTime(); + nStartingHeight = -1; vfSubscribe.assign(256, false); // Push a version message /// when NTP implemented, change to just nTime = GetAdjustedTime() int64 nTime = (fInbound ? GetAdjustedTime() : GetTime()); - PushMessage("version", VERSION, nLocalServices, nTime, addr); + // nBestHeight goes last so a node that predates it stops reading after + // addr and treats the rest as trailing bytes, which cost it a log line + // and nothing else. + PushMessage("version", VERSION, nLocalServices, nTime, addr, nBestHeight); } ~CNode() @@ -771,6 +795,22 @@ public: } } + template + void PushMessage(const char* pszCommand, const T1& a1, const T2& a2, const T3& a3, const T4& a4, const T5& a5) + { + try + { + BeginMessage(pszCommand); + vSend << a1 << a2 << a3 << a4 << a5; + EndMessage(); + } + catch (...) + { + AbortMessage(); + throw; + } + } + void PushRequest(const char* pszCommand, void (*fn)(void*, CDataStream&), void* param1) -- cgit v1.2.3 From f904c4bd4057ab7333c7d701fb40f3e87fedf7fd Mon Sep 17 00:00:00 2001 From: Bitflash Date: Fri, 31 Jul 2026 11:27:53 -0300 Subject: gui: stop stacking every orphan the node ever produced on top of the list (#67) The wallet list sorted on GetDepthInMainChain() ascending so pending transactions came first. An orphaned coinbase is also depth 0 -- and it stays depth 0 forever, because it is never going to be confirmed. So every orphan a node had ever produced sat permanently at the top of the list, above every block it had actually won. A miner with a week of ordinary orphaned races opened the wallet and saw nothing but failures. The balance was right the whole time; the list was telling a different story. That is how this was found: a screenshot of a healthy, correctly-mining node that looked like it was failing at everything. Sort on (rank, depth) instead, where rank is 0 for pending, 1 for confirmed, 2 for orphaned. Orphans go to the bottom, pending stays on top, confirmed stays ordered most-recent-first. Verified against a datadir whose chain had been reorganised away, so every coinbase in it is orphaned: all 17 wallet transactions classify as rank 2. Not tested against a mixed wallet -- the machine that has one is in production with its datadir locked by the running node. --- src/gui.cpp | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 28f5ffa..53cb6db 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -124,15 +124,32 @@ static void RefreshWallet() g_txRows.clear(); TRY_CRITICAL_BLOCK(cs_mapWallet) { - // Sort: unconfirmed first (depth 0), then by depth ascending (least - // deep = most recent). We can't use nTimeReceived because a node - // resync re-stamps all txs with the same time. Depth is always correct. - std::vector> vs; - for (auto& kv : mapWallet) - vs.push_back({kv.second.GetDepthInMainChain(), kv.second.GetHash()}); + // Sort: pending first, then confirmed by depth ascending (least deep = + // most recent), then orphaned coinbases at the very bottom. + // + // Orphans have to be pushed down explicitly. They are depth 0, exactly + // like a genuinely pending transaction, so sorting on depth alone put + // every orphan the node had ever produced at the top of the list -- and + // they never age out, because depth 0 is where they stay forever. A + // miner with a week of ordinary orphaned races opened the wallet and + // saw nothing but failures stacked above every block it had actually + // won. The balance was right; the list was telling a different story. + // + // We can't sort on nTimeReceived because a resync re-stamps every + // transaction with the same time. Depth is always correct. + // + // 0 = pending, 1 = confirmed, 2 = orphaned. + std::vector,uint256>> vs; + for (auto& kv : mapWallet) { + int d = kv.second.GetDepthInMainChain(); + bool orphaned = kv.second.IsCoinBase() && kv.second.hashBlock != 0 && d == 0; + int rank = orphaned ? 2 : (d == 0 ? 0 : 1); + vs.push_back({{rank, d}, kv.second.GetHash()}); + } std::sort(vs.begin(), vs.end(), - [](const std::pair& a, const std::pair& b) { - return a.first < b.first; // 0 (unconfirmed) first, then shallowest + [](const std::pair,uint256>& a, + const std::pair,uint256>& b) { + return a.first < b.first; }); for (auto& sv : vs) { auto mi = mapWallet.find(sv.second); -- cgit v1.2.3 From 24ed292f0524f1bc77c317b60d7ecf265596b3dd Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 12:23:31 -0300 Subject: diag: let the node say what is wrong with it (#81) * Bump to 1.2.11 One change, and it is the reason a node on Windows kept collecting peers it could not hear. FD_SETSIZE was never defined in this tree, so it defaulted to 64 and select() could watch the listen socket plus 63 peers -- everything past that stayed connected and was never read again. Windows FD_SET stops adding once its array is full and says nothing, so there was no error to notice. Because new connections go on the end of vNodes, it was always the newest connection that starved, including the ones dialed to fix connectivity. Measured on two nodes: 277 and 90 CNode objects against 93 and 90 live sockets, 316 and 355 sockets in a bound-but-never-connected state, and a status bar reporting hundreds of peers for a node really talking to thirty. 1.2.11 raises FD_SETSIZE to 1024 on Windows, caps connections at 125, drops a node stuck undeliverable after 60 seconds instead of holding it forever, stops answering a failed select() with POSIX code that has no meaning on Windows, and warns when there are more peers than select can watch. * diag: let the node say what is wrong with it Every hard problem in this project so far was found from outside the node -- sockets read over WMI, committed memory compared against a README, a grep over somebody debug.log. In each case the node knew and had no way to say so. One report, rendered identically for the GUI panel and the log: peers held vs peers actually watched by select(), blocks received and how many arrived without a parent, proof-of-work mode with the live miner thread count, and per peer the age of the last message in each direction plus anything stuck unsent. --- Makefile | 2 +- src/gui.cpp | 38 +++++++++++++++++++ src/main.cpp | 7 ++++ src/net.cpp | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/net.h | 19 ++++++++++ 5 files changed, 182 insertions(+), 1 deletion(-) (limited to 'src/gui.cpp') diff --git a/Makefile b/Makefile index d436885..0bc5892 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ROOT := $(shell pwd) NPROC := $(shell nproc 2>/dev/null || echo 2) SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") -VERSION := 1.2.10 +VERSION := 1.2.11 # ---- Linux ---------------------------------------------------------------- diff --git a/src/gui.cpp b/src/gui.cpp index 53cb6db..db6b69e 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -50,6 +50,7 @@ static int64 g_lastWalletRefresh = 0; static bool g_showSend = false; static bool g_showOptions = false; static bool g_showAbout = false; +static bool g_showDiagnostics = false; static bool g_needRefresh = true; static char g_sendAddr[128] = {}; @@ -310,6 +311,7 @@ static void DrawMainWindow() ImGui::EndMenu(); } if (ImGui::MenuItem("Options")) g_showOptions = true; + if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; if (ImGui::MenuItem("About")) g_showAbout = true; ImGui::EndMenuBar(); } @@ -815,6 +817,41 @@ static void DrawOptionsDialog() // --------------------------------------------------------------------------- // About dialog // --------------------------------------------------------------------------- +// What the node knows about itself, in the words it writes to the log. Same +// text in both places on purpose: what a user pastes into an issue is then +// exactly what a developer has already read a hundred times. +static void DrawDiagnosticsDialog() +{ + if (!g_showDiagnostics) return; + ImGui::SetNextWindowSize(ImVec2(720.0f, 460.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Diagnostics", &g_showDiagnostics, ImGuiWindowFlags_NoCollapse)) + { + // Rebuilt at most once a second: it walks every peer and takes their + // send locks, which is not something to do at frame rate. + static std::string strReport; + static int64 nLastBuilt = 0; + if (GetTime() != nLastBuilt) + { + nLastBuilt = GetTime(); + strReport = GetDiagnosticsText(); + } + + if (ImGui::Button("Copy to clipboard", ImVec2(160.0f, 0.0f))) + ImGui::SetClipboardText(strReport.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("The same report goes to debug.log every ten minutes."); + ImGui::Separator(); + + ImGui::BeginChild("##diagtext", ImVec2(0.0f, 0.0f), false, + ImGuiWindowFlags_HorizontalScrollbar); + ImGui::TextUnformatted(strReport.c_str()); + ImGui::EndChild(); + } + ImGui::End(); +} + static void DrawAboutDialog() { if (!g_showAbout) return; @@ -921,6 +958,7 @@ int RunGUI(int argc, char* argv[]) DrawMainWindow(); DrawSendDialog(); DrawOptionsDialog(); + DrawDiagnosticsDialog(); DrawAboutDialog(); DrawStatusBar(); diff --git a/src/main.cpp b/src/main.cpp index 5610de9..1a4c9ea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1494,6 +1494,11 @@ bool CBlock::AcceptBlock() bool ProcessBlock(CNode* pfrom, CBlock* pblock) { + // Counted before any check, so the ratio below is against everything that + // arrived rather than everything that was any good. + if (pfrom) + nBlocksReceived++; + // Check for duplicate uint256 hash = pblock->GetHash(); if (mapBlockIndex.count(hash)) @@ -1512,6 +1517,8 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock) if (!mapBlockIndex.count(pblock->hashPrevBlock)) { printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,14).c_str()); + if (pfrom) + nBlocksWithoutParent++; mapOrphanBlocks.insert(make_pair(hash, pblock)); mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); diff --git a/src/net.cpp b/src/net.cpp index 5ef8316..19893ee 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -46,6 +46,96 @@ CCriticalSection cs_mapRelay; map mapAlreadyAskedFor; string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) +int nPeersWatched = 0; +int64 nBlocksReceived = 0; +int64 nBlocksWithoutParent = 0; +static int64 nNodeStartTime = 0; + +// Defined further down, next to the counter it reads. The miner keeps its own +// live count -- threads bump it on the way in and on the way out -- which is +// the honest number here: how many are hashing now, not how many were asked to. +static int MinersRunningCount(); + +// Seconds rendered the way a person reads them off a screen. +static string FormatAge(int64 nSeconds) +{ + if (nSeconds < 0) return "never"; + if (nSeconds < 60) return strprintf("%llds", (long long)nSeconds); + if (nSeconds < 3600) + return strprintf("%lldm%02llds", (long long)(nSeconds / 60), (long long)(nSeconds % 60)); + return strprintf("%lldh%02lldm", (long long)(nSeconds / 3600), (long long)((nSeconds % 3600) / 60)); +} + +string GetDiagnosticsText() +{ + int64 nNow = GetTime(); + string str; + + int nInbound = 0, nHeld = 0; + vector vCopy; + CRITICAL_BLOCK(cs_vNodes) + { + vCopy = vNodes; + nHeld = (int)vNodes.size(); + foreach(CNode* pnode, vNodes) + if (pnode->fInbound) + nInbound++; + } + + int nMedian = GetPeerMedianHeight(); + + str += "Bitflash node diagnostics\n"; + str += strprintf(" uptime %s\n", + FormatAge(nNodeStartTime ? nNow - nNodeStartTime : -1).c_str()); + if (nMedian < 0) + str += strprintf(" height %d (no peer has said where it is)\n", nBestHeight); + else + str += strprintf(" height %d (peers report %d, %s)\n", + nBestHeight, nMedian, + nBestHeight >= nMedian ? "level or ahead" + : strprintf("behind by %d", nMedian - nBestHeight).c_str()); + str += strprintf(" peers held %d (%d inbound, %d outbound)\n", + nHeld, nInbound, nHeld - nInbound); + + // The number that would have made the deafness obvious. Anything held but + // not watched is a socket this node will never read again. + str += strprintf(" peers watched %d of %d that select() can hold%s\n", + nPeersWatched, (int)FD_SETSIZE - 1, + nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); + + if (nBlocksReceived > 0) + str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", + (long long)nBlocksReceived, (long long)nBlocksWithoutParent, + 100.0 * nBlocksWithoutParent / nBlocksReceived); + else + str += " blocks received 0\n"; + + int nMining = MinersRunningCount(); + str += strprintf(" proof of work %s mode%s\n", + RandomXFastReady() ? "fast (2 GB dataset)" : "light (256 MB cache)", + nMining > 0 + ? strprintf(", mining on %d thread(s), about %d MB", + nMining, + (RandomXFastReady() ? 2080 : 256) + 2 * nMining).c_str() + : ", not mining"); + + str += "\n peer dir height last recv last send unsent\n"; + foreach(CNode* pnode, vCopy) + { + int nSendSize = 0; + TRY_CRITICAL_BLOCK(pnode->cs_vSend) + nSendSize = (int)pnode->vSend.size(); + str += strprintf(" %-28s %-4s %6d %10s %10s %7d\n", + pnode->addr.ToString().substr(0, 28).c_str(), + pnode->fInbound ? "in" : "out", + pnode->nStartingHeight, + FormatAge(pnode->nLastRecv ? nNow - pnode->nLastRecv : -1).c_str(), + FormatAge(pnode->nLastSend ? nNow - pnode->nLastSend : -1).c_str(), + nSendSize); + } + return str; +} + bool GetMyExternalIP(unsigned int& ipRet) { @@ -1058,6 +1148,24 @@ void ThreadSocketHandler2(void* parg) (int)vNodes.size() - (int)FD_SETSIZE + 1); } } + nPeersWatched = (int)nWatched - 1; // less the listen socket + } + + // Say the state of the node out loud now and then, so a log pulled off + // a machine three weeks later still answers "was it hearing anybody". + { + static int64 nLastReport = 0; + // Wound forward on the first pass so the first report lands about a + // minute in, once peers have had time to connect. A report written + // at second zero says nothing, and waiting ten minutes for the + // first one is ten minutes of a log that cannot answer anything. + if (nLastReport == 0) + nLastReport = GetTime() - 9 * 60; + if (GetTime() - nLastReport > 10 * 60) + { + nLastReport = GetTime(); + printf("%s\n", GetDiagnosticsText().c_str()); + } } vfThreadRunning[0] = false; @@ -1432,6 +1540,14 @@ void ThreadMessageHandler2(void* parg) static CCriticalSection cs_nMinersRunning; static int nMinersRunning = 0; +static int MinersRunningCount() +{ + int n = 0; + CRITICAL_BLOCK(cs_nMinersRunning) + n = nMinersRunning; + return n; +} + void ThreadBitcoinMiner(void* parg) { int nThreadId = (int)(intptr_t)parg; @@ -1490,6 +1606,7 @@ int StartMinerThreads() bool StartNode(string& strError) { strError = ""; + nNodeStartTime = GetTime(); // Sockets startup WSADATA wsadata; diff --git a/src/net.h b/src/net.h index 01f314d..bd4eb77 100644 --- a/src/net.h +++ b/src/net.h @@ -102,6 +102,25 @@ static const int BTF_PING_INTERVAL_SECS = 10 * 60; static const int BTF_BEHIND_GRACE_SECS = 5 * 60; static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60; +// --- Self-diagnosis ------------------------------------------------------- +// +// A node that has stopped working looks exactly like a node with nothing to +// do. That has cost this project real time more than once: a node deaf to +// most of its peers, a miner allocating one RandomX dataset per thread, a +// third of arriving blocks missing their parent -- each was found from +// outside, by reading the process's sockets or its memory from another +// machine, because the node itself had no way to say so. +// +// These counters exist so it can. Cheap to keep, and they name the failures +// that actually happened rather than the ones that sound impressive. +extern int nPeersWatched; // peers in the last select() set +extern int64 nBlocksReceived; // blocks handed to ProcessBlock +extern int64 nBlocksWithoutParent; // ...of those, how many arrived orphaned + +// One report, rendered the same way for the GUI panel and the log, so what a +// user pastes into an issue is what a developer already knows how to read. +string GetDiagnosticsText(); + // Defined in main.cpp. Declared here because CNode announces it in the version // message, and net.h is included before main.h. extern int nBestHeight; -- cgit v1.2.3 From b426948b966405b65f3ea6c1994164b462980694 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 12:44:04 -0300 Subject: mine: remember whether this node was mining, with the command line winning (#84) --- src/db.cpp | 39 +++++++++++++++++++++++++++------------ src/gui.cpp | 7 +++++++ src/main.cpp | 1 + src/main.h | 9 ++++++--- src/main_gui.cpp | 5 +++++ 5 files changed, 46 insertions(+), 15 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/db.cpp b/src/db.cpp index 16f2e91..a503cae 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -642,16 +642,29 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) ssKey >> strKey; if (strKey == "nTransactionFee") ssValue >> nTransactionFee; if (strKey == "addrIncoming") ssValue >> addrIncoming; - // Mining-mode/pool settings (nMineMode, strParticipantPool, strPoolName, - // strPoolDashboardUrl, dPoolFeePercent, fGenerateBitcoins) are intentionally - // NOT restored here. They used to be, and LoadWallet() runs after CLI flags - // are parsed in main_gui.cpp, so a saved value would silently overwrite - // whatever was just requested on the command line or in a previous Options - // session -- no error, no log line. That's what caused pool name/address to - // show correctly in the UI but stay stale in the logs, and mode to change - // behavior across restarts. Mode/pool config is decided fresh every launch - // instead: CLI flags if given, otherwise the compiled-in default. Nothing to - // go stale, nothing to fight over load order. + + // Mining mode is restored, but only when no flag chose one. + // + // These settings used to be restored unconditionally. LoadWallet() + // runs after main_gui.cpp has parsed the command line, so a stored + // value silently overwrote what had just been asked for -- no error, + // no log line -- and the fix at the time was to stop restoring them + // at all. That traded a silent override for a different silent + // failure: a machine set to mine came back from every restart as a + // plain relay, looking exactly like one that had been asked to + // relay. A 32-core miner sat idle for hours that way, and the only + // evidence was memory usage that never climbed. + // + // Precedence is now explicit rather than accidental: a flag wins, + // and it says so in the log below. Only the two fields that decide + // whether this node mines are restored, plus the pool address a + // participant cannot work without. + if (!fMineModeFromCommandLine) + { + if (strKey == "nMineMode") ssValue >> nMineMode; + if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins; + if (strKey == "strParticipantPool") ssValue >> strParticipantPool; + } } } } @@ -673,8 +686,10 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) printf("nTransactionFee = %lld\n", nTransactionFee); printf("addrIncoming = %s\n", addrIncoming.ToString().c_str()); - printf("nMineMode = %d, strParticipantPool = %s, fGenerateBitcoins = %d (from CLI/default, not wallet.dat)\n", - nMineMode, strParticipantPool.c_str(), fGenerateBitcoins); + printf("nMineMode = %d, strParticipantPool = %s, fGenerateBitcoins = %d (%s)\n", + nMineMode, strParticipantPool.c_str(), fGenerateBitcoins, + fMineModeFromCommandLine ? "from the command line, wallet.dat ignored" + : "remembered from wallet.dat, or the default"); printf("strPoolName = %s, dPoolFeePercent = %.2f, strPoolDashboardUrl = %s (from CLI/default, not wallet.dat)\n", strPoolName.c_str(), dPoolFeePercent, strPoolDashboardUrl.c_str()); diff --git a/src/gui.cpp b/src/gui.cpp index db6b69e..e2bb5cf 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -795,6 +795,13 @@ static void DrawOptionsDialog() if (!vfThreadRunning[3]) StartMinerThreads(); } + // Remember the choice. Without this the node came back from every + // restart as a plain relay, and a machine that had been mining for + // days looked identical to one that had never been asked to. + CWalletDB().WriteSetting("nMineMode", nMineMode); + CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); + CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); + if (nMineMode == MINE_OPERATOR) { if (!gPoolServerRunning) { gPoolServerRunning = true; diff --git a/src/main.cpp b/src/main.cpp index 1a4c9ea..f63f575 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -91,6 +91,7 @@ int nMinerThreads = 0; int64 nTransactionFee = 0; CAddress addrIncoming; int nMineMode = MINE_RELAY; +bool fMineModeFromCommandLine = false; string strParticipantPool; // participant mode: pool .btf address string strPoolName = "Bitflash Pool"; string strPoolDashboardUrl; diff --git a/src/main.h b/src/main.h index 7266e3d..a50485f 100644 --- a/src/main.h +++ b/src/main.h @@ -64,9 +64,8 @@ extern CBlockIndex* pindexBest; extern unsigned int nTransactionsUpdated; extern string strSetDataDir; extern int nDropMessagesTest; -// Mining mode -- persisted in wallet.dat -// Mining mode -- decided fresh every launch (CLI flags, else default). Never -// restored from wallet.dat; see LoadWallet() in db.cpp. +// Mining mode -- remembered in wallet.dat, but the command line always wins. +// See LoadWallet() in db.cpp for why the precedence has to be explicit. // MINE_RELAY (0): default. Node runs/syncs, never mines, no pool server. // MINE_SOLO (1): mine to own wallet, no pool server // MINE_OPERATOR (2): run pool server, mine to own wallet, distribute to miners @@ -76,6 +75,10 @@ extern int nDropMessagesTest; #define MINE_OPERATOR 2 #define MINE_PARTICIPANT 3 extern int nMineMode; +// Set by the startup parser when a flag chose the mode. LoadWallet leaves the +// stored mode alone when this is true, so a flag is never silently overruled by +// something a previous session wrote. +extern bool fMineModeFromCommandLine; extern string strParticipantPool; // participant: pool .btf address extern string strPoolName; // operator: announced pool name extern string strPoolDashboardUrl; // operator: optional dashboard URL diff --git a/src/main_gui.cpp b/src/main_gui.cpp index c593162..5c87fe0 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -97,6 +97,7 @@ static void ParseStartupArguments(int argc, char* argv[]) // their own mode below, so only the unqualified case is affected. if (nMineMode == MINE_RELAY) nMineMode = MINE_SOLO; + fMineModeFromCommandLine = true; } if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine")) @@ -127,12 +128,16 @@ static void ParseStartupArguments(int argc, char* argv[]) } if (arg(argc,argv,"/operator") || arg(argc,argv,"-operator")) + { nMineMode = MINE_OPERATOR; + fMineModeFromCommandLine = true; + } if (arg(argc,argv,"/participant") || arg(argc,argv,"-participant")) { nMineMode = MINE_PARTICIPANT; strParticipantPool = argval2(argc, argv, "/participant", "-participant"); + fMineModeFromCommandLine = true; } string poolName = argval2(argc, argv, "/poolname", "-poolname"); -- cgit v1.2.3 From 9da2183bb16e4c32cafbb71eeadc7f112b48ab05 Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:45:26 -0300 Subject: Add wallet safety backup panel (#89) Co-authored-by: Gilcimar Duarte --- README.md | 23 ++++++---- src/gui.cpp | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 10 deletions(-) (limited to 'src/gui.cpp') diff --git a/README.md b/README.md index 16e979f..28030f1 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,20 @@ It loads the wallet, writes the copy, and exits without starting the node. If you would rather copy by hand, shut the node down first and take the **whole** data directory, not just `wallet.dat`. -**Every backup is a snapshot, so repeat it.** Keys are generated when they are -needed rather than derived from a seed, so a backup contains only the keys that -existed the moment it was taken. Mine a block or receive to a new address -afterwards, and those coins sit on-chain at an address whose private key is in -no backup you hold. Back up again whenever you use a new address. - -The second point is the pre-BIP32 problem that deterministic wallets were -invented to solve; see [#47](https://github.com/Bitflash-sh/bitflash/issues/47). -Until that lands, repetition is the only defence. +**Every backup is still a snapshot, but it now has a safety margin.** The wallet +keeps a pool of 100 pre-generated keys. A backup contains those keys, so it +covers the next 100 mining rewards or receive addresses the node hands out after +the backup. That makes a normal backup much safer than the original 0.1.0 +wallet, where the very next block could land on a key the backup did not have. + +It is not a seed phrase. Heavy use can drain the key pool, and deterministic +wallet restore is still tracked in [#47](https://github.com/Bitflash-sh/bitflash/issues/47). +Back up again after mining for a while, after creating many receive addresses, +and before moving the wallet to another machine. + +The GUI has a **Backup Wallet** button and a **Wallet Safety** view. Use that if +you do not want to run the command by hand; it fills the key pool before writing +the backup. ### When wallet.dat itself will not open 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; static bool g_showOptions = false; static bool g_showAbout = false; static bool g_showDiagnostics = false; +static bool g_showWalletSafety = false; static bool g_needRefresh = true; static char g_sendAddr[128] = {}; static char g_sendAmount[32] = {}; static std::string g_sendStatus; +static char g_backupPath[512] = {}; +static std::string g_backupStatus; +static int64 g_lastWalletBackup = 0; +static bool g_walletSafetyLoaded = false; static char g_participantPool[256] = {}; static char g_poolName[128] = {}; static char g_poolFee[32] = {}; @@ -115,8 +120,38 @@ static std::string PoolLabel(const BtfPoolAnnouncement& ann) return o.str(); } +static std::string BackupTimeSuffix() +{ + return strprintf("%lld", (long long)GetTime()); +} + +static void SetDefaultBackupPath() +{ + std::string path = GetAppDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; + strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); + g_backupPath[sizeof(g_backupPath)-1] = '\0'; +} + +static void LoadWalletSafetyState() +{ + if (g_walletSafetyLoaded) + return; + CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); + SetDefaultBackupPath(); + g_walletSafetyLoaded = true; +} + +static int KeyPoolCount() +{ + int n = 0; + CRITICAL_BLOCK(cs_keyPool) + n = (int)mapKeyPool.size(); + return n; +} + static void RefreshWallet() { + LoadWalletSafetyState(); std::vector vchPubKey; if (CWalletDB("r").ReadDefaultKey(vchPubKey)) g_myAddress = PubKeyToAddress(vchPubKey); @@ -307,10 +342,12 @@ static void DrawMainWindow() // ---- Menu bar ---- if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Backup Wallet")) g_showWalletSafety = true; if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); ImGui::EndMenu(); } if (ImGui::MenuItem("Options")) g_showOptions = true; + if (ImGui::MenuItem("Wallet Safety")) g_showWalletSafety = true; if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; if (ImGui::MenuItem("About")) g_showAbout = true; ImGui::EndMenuBar(); @@ -340,6 +377,9 @@ static void DrawMainWindow() if (ImGui::Button(" Send Coins ")) g_showSend = true; ImGui::PopStyleColor(2); + ImGui::SameLine(0.0f, 8.0f); + if (ImGui::Button(" Backup Wallet ")) g_showWalletSafety = true; + ImGui::SameLine(0.0f, 20.0f); if (fGenerateBitcoins) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); @@ -701,7 +741,7 @@ static void DrawOptionsDialog() } wasOpen = true; - ImGui::SetNextWindowSize(ImVec2(500.0f, 335.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(540.0f, 385.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Options", &g_showOptions, @@ -722,6 +762,14 @@ static void DrawOptionsDialog() ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); + if (g_mineRadio != MINE_RELAY && g_lastWalletBackup == 0) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + 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); + ImGui::PopStyleColor(); + if (ImGui::SmallButton("Backup now")) g_showWalletSafety = true; + } + if (g_mineRadio == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Pool Announcement"); @@ -821,6 +869,100 @@ static void DrawOptionsDialog() ImGui::End(); } +// --------------------------------------------------------------------------- +// Wallet safety dialog +// --------------------------------------------------------------------------- +static void DrawWalletSafetyDialog() +{ + if (!g_showWalletSafety) return; + LoadWalletSafetyState(); + + ImGui::SetNextWindowSize(ImVec2(650.0f, 390.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::SeparatorText("Backup"); + if (g_lastWalletBackup > 0) + ImGui::Text("Last GUI backup: %s", DateTimeStr(g_lastWalletBackup).c_str()); + else + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), + "Last GUI backup: never"); + + ImGui::TextDisabled("Data directory:"); + ImGui::SameLine(); + ImGui::TextWrapped("%s", GetAppDir().c_str()); + + ImGui::Spacing(); + ImGui::Text("Backup file:"); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputText("##backup-path", g_backupPath, sizeof(g_backupPath)); + + if (ImGui::Button("Use new default name", ImVec2(170.0f, 0.0f))) + SetDefaultBackupPath(); + ImGui::SameLine(); + if (ImGui::Button("Copy data dir", ImVec2(120.0f, 0.0f))) + ImGui::SetClipboardText(GetAppDir().c_str()); + + ImGui::Spacing(); + if (ImGui::Button("Write Backup", ImVec2(150.0f, 0.0f))) + { + std::string path = g_backupPath; + if (path.empty()) + { + g_backupStatus = "Choose a backup file first."; + } + else if (FileExists(path.c_str())) + { + g_backupStatus = "Refusing to overwrite an existing file. Use a new name."; + } + else + { + TopUpKeyPool(); + if (BackupWallet(path)) + { + g_lastWalletBackup = GetTime(); + CWalletDB().WriteSetting("nLastWalletBackup", g_lastWalletBackup); + g_backupStatus = "Backup written: " + path; + SetDefaultBackupPath(); + } + else + { + g_backupStatus = "Backup failed. See debug.log for the detailed error."; + } + } + } + + if (!g_backupStatus.empty()) + { + ImVec4 col = g_backupStatus.find("Backup written") == 0 + ? ImVec4(0.35f, 1.0f, 0.45f, 1.0f) + : ImVec4(1.0f, 0.45f, 0.35f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, col); + ImGui::TextWrapped("%s", g_backupStatus.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Spacing(); + ImGui::SeparatorText("Key Pool"); + int nPool = KeyPoolCount(); + ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE); + ImGui::ProgressBar(KEYPOOL_SIZE > 0 ? (float)nPool / (float)KEYPOOL_SIZE : 0.0f, + ImVec2(-1.0f, 0.0f)); + ImGui::TextWrapped( + "A backup contains the keys that already exist. The key pool keeps " + "future mining rewards and receive addresses inside the next backup " + "window, but it is not a seed phrase. Back up again after heavy use."); + + ImGui::Spacing(); + ImGui::Separator(); + if (ImGui::Button("Close", ImVec2(90.0f, 0.0f))) + g_showWalletSafety = false; + } + ImGui::End(); +} + // --------------------------------------------------------------------------- // About dialog // --------------------------------------------------------------------------- @@ -965,6 +1107,7 @@ int RunGUI(int argc, char* argv[]) DrawMainWindow(); DrawSendDialog(); DrawOptionsDialog(); + DrawWalletSafetyDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); DrawStatusBar(); -- cgit v1.2.3 From ae81fa37123730982c4e054d7433c9d0c944a373 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 14:48:12 -0300 Subject: wallet: keep the default backup out of the directory it protects (#90) Two follow-ups to #89. The default backup path was inside the data directory -- same disk, same folder the README says to copy wholesale, same folder the user eventually deletes, which is the moment the backup was for. It now defaults to the home directory and the dialog says so when a path under the data directory is chosen anyway. And nLastWalletBackup was written by the GUI, so anyone backing up with /backupwallet -- the method the README documents first -- was told forever that they had never backed up. BackupWallet records it now, so both paths count. --- src/db.cpp | 9 +++++++++ src/gui.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/db.cpp b/src/db.cpp index 7763af8..d482c5a 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -834,6 +834,15 @@ bool BackupWallet(const string& strDest) { return error("BackupWallet() : %s\n", e.what()); } } + // Recorded here rather than by whoever called, so that a backup taken with + // /backupwallet counts as much as one taken from the GUI. When the GUI + // wrote this itself, anyone following the README -- which documents the + // command line first -- was told forever that they had never backed up. + // Outside the cs_db block above: this opens the wallet for writing, and + // there is no reason to do that while holding the database lock. + try { CWalletDB().WriteSetting("nLastWalletBackup", (int64)GetTime()); } + catch (...) { } // the backup is written; failing to note it is not fatal + printf("BackupWallet() : wrote %s\n", strDest.c_str()); return true; } diff --git a/src/gui.cpp b/src/gui.cpp index df61bb1..51b5d5f 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -125,9 +125,47 @@ static std::string BackupTimeSuffix() return strprintf("%lld", (long long)GetTime()); } +// Where to put a backup by default. +// +// Not the data directory. A copy that lives in the folder it is meant to +// outlive is not a backup: same disk, same directory the README tells people to +// copy wholesale, and the same directory they are eventually going to delete -- +// which is the moment the backup was for. The home directory is not off-site +// either, but it survives the one accident this file exists to survive. +static std::string DefaultBackupDir() +{ +#ifdef _WIN32 + const char* pszHome = getenv("USERPROFILE"); +#else + const char* pszHome = getenv("HOME"); +#endif + if (pszHome && *pszHome) + return std::string(pszHome); + return GetAppDir(); // nothing better available; the warning below still fires +} + +// True when a path sits inside the data directory, so the dialog can say so. +static bool PathIsInsideDataDir(const std::string& strPath) +{ + std::string strDir = GetAppDir(); + if (strPath.size() < strDir.size()) + return false; + std::string a = strPath.substr(0, strDir.size()); + // Windows paths are case-insensitive and mix separators; compare loosely. + for (size_t i = 0; i < a.size(); i++) + { + char c1 = a[i], c2 = strDir[i]; + if (c1 == '\\') c1 = '/'; + if (c2 == '\\') c2 = '/'; + if (tolower((unsigned char)c1) != tolower((unsigned char)c2)) + return false; + } + return true; +} + static void SetDefaultBackupPath() { - std::string path = GetAppDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; + std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); g_backupPath[sizeof(g_backupPath)-1] = '\0'; } @@ -899,6 +937,16 @@ static void DrawWalletSafetyDialog() ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##backup-path", g_backupPath, sizeof(g_backupPath)); + if (PathIsInsideDataDir(g_backupPath)) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + ImGui::TextWrapped( + "This path is inside the data directory. A backup kept there is lost " + "with the thing it was protecting -- put it on another disk, or at " + "least somewhere you would not delete along with the node's files."); + ImGui::PopStyleColor(); + } + if (ImGui::Button("Use new default name", ImVec2(170.0f, 0.0f))) SetDefaultBackupPath(); ImGui::SameLine(); @@ -922,8 +970,9 @@ static void DrawWalletSafetyDialog() TopUpKeyPool(); if (BackupWallet(path)) { - g_lastWalletBackup = GetTime(); - CWalletDB().WriteSetting("nLastWalletBackup", g_lastWalletBackup); + // BackupWallet records the time itself, so a backup taken + // with /backupwallet counts the same as one taken here. + CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); g_backupStatus = "Backup written: " + path; SetDefaultBackupPath(); } -- cgit v1.2.3 From 7de7ef89d3365f8f9c2eb2b6ed0193c3033e221c Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 17:57:07 -0300 Subject: wallet: create and restore a recovery phrase (#96) Adds -newphrase, -restorephrase, -restoredepth, -newaddress and -showderived, plus the window buttons that reach the same code. Restore derives forward in batches of 100 and rescans after each, stopping when a whole batch turns up nothing; the phrase is shown on the terminal only, never in debug.log. Proved end to end on a live wallet: paid a derived address, deleted wallet.dat, restored from the twelve words and recovered the coin. Coins on the pre-seed default address do not come back, and the command says so. --- src/Makefile | 4 +- src/gui.cpp | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main_gui.cpp | 38 ++++++++ src/makefile.mingw | 4 +- src/selftest.cpp | 30 +------ src/util.h | 27 ++++++ src/walletcmd.cpp | 237 ++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 43 +++++++++ 8 files changed, 610 insertions(+), 31 deletions(-) create mode 100644 src/walletcmd.cpp create mode 100644 src/walletcmd.h (limited to 'src/gui.cpp') diff --git a/src/Makefile b/src/Makefile index bbdc9e2..3eaf2ba 100644 --- a/src/Makefile +++ b/src/Makefile @@ -42,7 +42,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h compat.h + selftest.h walletcmd.h compat.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -50,7 +50,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o + obj/selftest.o obj/bip32.o obj/walletcmd.o GUI_OBJS = obj/gui.o obj/main_gui.o diff --git a/src/gui.cpp b/src/gui.cpp index 51b5d5f..e800244 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -12,6 +12,11 @@ #include #include #include +#include +#include +#include +#include "bip32.h" +#include "walletcmd.h" #include "font_roboto.h" // --------------------------------------------------------------------------- @@ -910,6 +915,220 @@ static void DrawOptionsDialog() // --------------------------------------------------------------------------- // Wallet safety dialog // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Recovery phrase +// +// The phrase is shown once, and the seed is installed only after the user says +// they have written it down. Generating it and storing it in the same breath +// would hand somebody a wallet whose only backup is twelve words they may have +// closed the window on -- which is worse than no phrase at all, because they +// would believe they had one. +// +// Nothing here writes the words to disk or to debug.log. They live in one +// std::string that is cleared the moment the dialog closes. +// --------------------------------------------------------------------------- +static bool g_showCreatePhrase = false; +static bool g_showRestorePhrase = false; +static std::string g_pendingMnemonic; +static bool g_phraseWrittenDown = false; +static std::string g_phraseStatus; + +static char g_restoreInput[600] = {}; +static std::string g_restoreStatus; +static std::atomic g_restoreRunning(false); +static std::atomic g_restoreDerived(0); +static std::atomic g_restoreRecovered(0); +static std::string g_restoreResult; +static std::mutex g_restoreResultMutex; + +static void RestoreProgress(void*, int nDerived, int nRecovered) +{ + g_restoreDerived.store(nDerived); + g_restoreRecovered.store(nRecovered); +} + +static void RestoreThread(std::string strPhrase) +{ + std::string strError; + int nRecovered = 0, nDerived = 0; + bool fOk = RestoreFromPhrase(strPhrase, 0, RestoreProgress, NULL, + strError, nRecovered, nDerived); + { + std::lock_guard lock(g_restoreResultMutex); + if (!fOk) + g_restoreResult = "Nothing was changed. " + strError; + else if (nRecovered > 0) + g_restoreResult = strprintf( + "Restored %d transaction(s) across %d derived addresses. " + "Restart the node to see the balance.", nRecovered, nDerived); + else + g_restoreResult = strprintf( + "No transactions found for the first %d addresses of that phrase. " + "Check the words and their order.", nDerived); + } + g_restoreRunning.store(false); + g_needRefresh = true; +} + +static void DrawCreatePhraseDialog() +{ + if (!g_showCreatePhrase) return; + + ImGui::SetNextWindowSize(ImVec2(640.0f, 340.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Recovery Phrase", &g_showCreatePhrase, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), + "Write these twelve words down, in order, on paper."); + ImGui::TextWrapped( + "They are the only thing that can rebuild this wallet if the file is lost. " + "Anyone who reads them can spend your coins. They are not stored anywhere, " + "and this is the only time they will be shown."); + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.10f, 1.0f)); + ImGui::BeginChild("##phrase", ImVec2(0.0f, 70.0f), true); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "%s", g_pendingMnemonic.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndChild(); + ImGui::PopStyleColor(); + + ImGui::Spacing(); + ImGui::TextWrapped( + "Keys already in this wallet are NOT covered by the phrase -- they existed " + "before it did. Keep your file backups as well."); + ImGui::Spacing(); + + ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown); + ImGui::Spacing(); + + if (!g_phraseWrittenDown) + ImGui::BeginDisabled(); + if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f))) + { + std::string strError; + if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) + { + TopUpKeyPool(); + g_phraseStatus = "Recovery phrase created. New addresses come from it."; + } + else + { + g_phraseStatus = "Could not install the seed: " + strError; + } + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_showCreatePhrase = false; + g_needRefresh = true; + } + if (!g_phraseWrittenDown) + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) + { + // Nothing was installed, so there is nothing to undo. The words go + // away with the string. + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_showCreatePhrase = false; + g_phraseStatus = "Cancelled. No recovery phrase was created."; + } + } + ImGui::End(); + + // Closing with the window X is the same as cancelling. + if (!g_showCreatePhrase && !g_pendingMnemonic.empty()) + { + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_phraseStatus = "Cancelled. No recovery phrase was created."; + } +} + +static void DrawRestorePhraseDialog() +{ + if (!g_showRestorePhrase) return; + + ImGui::SetNextWindowSize(ImVec2(640.0f, 330.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Restore From Phrase", &g_showRestorePhrase, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::TextWrapped( + "Type the twelve words, in order, separated by spaces. The node will " + "derive the addresses they describe and search the chain for their coins."); + ImGui::Spacing(); + + if (HaveHDSeed()) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + ImGui::TextWrapped( + "This wallet already has a recovery phrase. Restoring replaces it: " + "coins on addresses from the current phrase would no longer come back " + "from the words you wrote down for it. Back up wallet.dat first."); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } + + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextMultiline("##phrase-in", g_restoreInput, sizeof(g_restoreInput), + ImVec2(0.0f, 60.0f)); + + ImGui::Spacing(); + bool fBusy = g_restoreRunning.load(); + if (fBusy) + ImGui::BeginDisabled(); + if (ImGui::Button("Restore", ImVec2(140.0f, 0.0f))) + { + std::string strWhy; + if (!CanScanWalletTransactions(strWhy)) + { + std::lock_guard lock(g_restoreResultMutex); + g_restoreResult = strWhy; + } + else + { + { + std::lock_guard lock(g_restoreResultMutex); + g_restoreResult.clear(); + } + g_restoreDerived.store(0); + g_restoreRecovered.store(0); + g_restoreRunning.store(true); + // On its own thread: a restore walks the whole chain once per + // batch and would otherwise freeze the window for as long as it + // takes. + std::thread(RestoreThread, std::string(g_restoreInput)).detach(); + } + } + if (fBusy) + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Close", ImVec2(100.0f, 0.0f)) && !fBusy) + g_showRestorePhrase = false; + + ImGui::Spacing(); + if (fBusy) + { + ImGui::Text("Searching: %d addresses checked, %d transaction(s) so far", + g_restoreDerived.load(), g_restoreRecovered.load()); + } + else + { + std::lock_guard lock(g_restoreResultMutex); + if (!g_restoreResult.empty()) + ImGui::TextWrapped("%s", g_restoreResult.c_str()); + } + } + ImGui::End(); +} + static void DrawWalletSafetyDialog() { if (!g_showWalletSafety) return; @@ -993,6 +1212,43 @@ static void DrawWalletSafetyDialog() ImGui::PopStyleColor(); } + ImGui::Spacing(); + ImGui::SeparatorText("Recovery Phrase"); + if (HaveHDSeed()) + ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), + "This wallet has a recovery phrase."); + else + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), + "No recovery phrase. Only a file backup can rebuild this wallet."); + + if (!HaveHDSeed()) + { + if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) + { + std::string strError; + if (bitflash::BIP39GenerateMnemonic(g_pendingMnemonic, strError)) + { + g_phraseWrittenDown = false; + g_phraseStatus.clear(); + g_showCreatePhrase = true; + } + else + { + g_phraseStatus = "Could not generate a phrase: " + strError; + } + } + ImGui::SameLine(); + } + if (ImGui::Button("Restore from phrase", ImVec2(190.0f, 0.0f))) + g_showRestorePhrase = true; + + if (!g_phraseStatus.empty()) + ImGui::TextWrapped("%s", g_phraseStatus.c_str()); + + ImGui::TextWrapped( + "A phrase covers addresses created after it. Keys that already existed " + "are not derived from it, so keep the file backups above as well."); + ImGui::Spacing(); ImGui::SeparatorText("Key Pool"); int nPool = KeyPoolCount(); @@ -1157,6 +1413,8 @@ int RunGUI(int argc, char* argv[]) DrawSendDialog(); DrawOptionsDialog(); DrawWalletSafetyDialog(); + DrawCreatePhraseDialog(); + DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); DrawStatusBar(); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index aa9c23d..c9e7dee 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -2,6 +2,7 @@ #include "headers_core.h" #include "selftest.h" +#include "walletcmd.h" #include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 #include @@ -78,6 +79,11 @@ static void PrintUsage() printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); printf(" anyone, so guard it like cash)\n"); printf(" /importwallet=FILE (load keys from such a file back in)\n"); + printf(" /newaddress (print the next receiving address, then exit)\n"); + printf(" /newphrase (create a twelve-word recovery phrase for a\n"); + printf(" wallet that has none, show it once, exit)\n"); + printf(" /restorephrase=\"WORDS\" (rebuild this wallet from a phrase and scan\n"); + printf(" the chain for its coins, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -347,6 +353,38 @@ int main(int argc, char* argv[]) // /rescan -- walk the chain and pick up anything the wallet's keys own but // the wallet never recorded. Cheap to offer and the only recourse when a // balance is wrong for this reason. + string strShow = argval2(argc, argv, "/showderived", "-showderived"); + if (!strShow.empty()) + { + int nRet = CmdShowDerived(atoi(strShow.c_str())); + DBFlush(true); + return nRet; + } + + if (arg(argc,argv,"/newaddress") || arg(argc,argv,"-newaddress")) + { + int nRet = CmdNewAddress(); + DBFlush(true); + return nRet; + } + + if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) + { + int nRet = CmdNewPhrase(); + DBFlush(true); + return nRet; + } + + string strRestore = argval2(argc, argv, "/restorephrase", "-restorephrase"); + if (!strRestore.empty()) + { + string strDepth = argval2(argc, argv, "/restoredepth", "-restoredepth"); + int nMinDepth = strDepth.empty() ? 0 : atoi(strDepth.c_str()); + int nRet = CmdRestorePhrase(strRestore, nMinDepth); + DBFlush(true); + return nRet; + } + if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) { string strScanError; diff --git a/src/makefile.mingw b/src/makefile.mingw index 07ae034..94d2146 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -42,14 +42,14 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o + obj/selftest.o obj/bip32.o obj/walletcmd.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h + selftest.h walletcmd.h all: bitflash.exe diff --git a/src/selftest.cpp b/src/selftest.cpp index 6792ae7..b733c60 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -286,33 +286,9 @@ static int RunWalletKeyPoolSelfTest() return nFail == 0 ? 0 : 1; } -// A -mwindows binary starts with no console attached, so stdout goes nowhere -// even when it was launched from a terminal. Borrow the caller's console for -// the duration of the self-test. Nothing to do on Linux, where stdout is -// already connected. -static void AttachTerminal() -{ -#ifdef _WIN32 - // Only when there is nowhere for stdout to go. If the caller redirected it - // to a file or a pipe -- which is what `make tests > log` and any CI does -- - // that handle is inherited and already works, and reopening it on CONOUT$ - // would take the output away from the file and put it on the screen. That - // was the first version of this fix, and it broke the one case that - // mattered. - HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hOut != NULL && hOut != INVALID_HANDLE_VALUE) - return; - - if (AttachConsole(ATTACH_PARENT_PROCESS)) - { - // Return values ignored on purpose: if the reopen fails there is - // nowhere left to report it, and the exit status still carries the - // result. - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - } -#endif -} +// AttachTerminal() moved to util.h: the wallet phrase commands need it too, +// and two copies of a console-attach that must get its one condition right is +// one copy too many. static int RunWalletHDSelfTest() { diff --git a/src/util.h b/src/util.h index 25ef118..d95d3ae 100644 --- a/src/util.h +++ b/src/util.h @@ -293,6 +293,33 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp // Forward declaration -- defined in main.cpp string GetAppDir(); +// Give a command-line operation somewhere to print. +// +// The Windows binary is linked -mwindows and starts with no console, so stdout +// goes nowhere even when it was launched from a terminal. Commands that report +// to the person who typed them -- the self-tests, the wallet phrase commands -- +// call this first. +// +// Only when there is nowhere for stdout to go already: if the caller redirected +// it to a file or a pipe, that handle is inherited and works, and reopening it +// on CONOUT$ would take the output away from the file and put it on the screen. +inline void AttachTerminal() +{ +#ifdef _WIN32 + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut != NULL && hOut != INVALID_HANDLE_VALUE) + return; + if (AttachConsole(ATTACH_PARENT_PROCESS)) + { + // Return values ignored on purpose: if the reopen fails there is + // nowhere left to report it, and the exit status still carries the + // result. + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + } +#endif +} + inline int OutputDebugStringF(const char* pszFormat, ...) BF_FORMAT(1, 2); inline int OutputDebugStringF(const char* pszFormat, ...) diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp new file mode 100644 index 0000000..94e9d72 --- /dev/null +++ b/src/walletcmd.cpp @@ -0,0 +1,237 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Recovery-phrase commands, from the command line. +// +// The phrase reaches the person who typed the command and nowhere else. In this +// tree printf is OutputDebugStringF, which writes into the data directory's +// debug.log -- the file we routinely ask users to attach to issues. A wallet's +// twelve words must never go there, so this file takes the real printf and +// AttachTerminal() gives it somewhere to land on Windows. + +#include "headers_core.h" +#include "bip32.h" +#include "walletcmd.h" + +#undef printf + +// How far ahead to look for used addresses when restoring. +// +// A wallet is restored from a phrase alone, so the node has no idea how many +// addresses the old one handed out. It derives forward in batches and stops +// once a whole batch turns up unused. A fixed number would be simpler and wrong +// for exactly this network: a miner burns an address per block, so a node that +// mined for a week is thousands of keys deep, and stopping at a few hundred +// would restore a wallet that looks empty. +static const int RESTORE_BATCH = 100; +static const int RESTORE_MAX = 10000; + +int CmdNewPhrase() +{ + AttachTerminal(); + + if (HaveHDSeed()) + { + printf("This wallet already has a recovery phrase.\n"); + printf("Creating another would replace it, and coins on addresses derived from\n"); + printf("the current one would no longer come back from the phrase you write down.\n"); + printf("Nothing was changed.\n"); + return 1; + } + + std::string strMnemonic, strError; + if (!bitflash::BIP39GenerateMnemonic(strMnemonic, strError)) + { + fprintf(stderr, "Could not generate a phrase: %s\n", strError.c_str()); + return 1; + } + + if (!SetHDSeedFromMnemonic(strMnemonic, strError)) + { + fprintf(stderr, "Could not install the seed: %s\n", strError.c_str()); + return 1; + } + + // Fill the pool from the new seed straight away, so the addresses this + // wallet hands out next are ones the phrase can reproduce. + TopUpKeyPool(); + + printf("\n"); + printf("Write these twelve words down, in order, on paper.\n"); + printf("They are the only thing that can rebuild this wallet if the file is lost.\n"); + printf("Anyone who reads them can spend your coins. They are not stored anywhere,\n"); + printf("and this is the only time they will be shown.\n"); + printf("\n"); + printf(" %s\n", strMnemonic.c_str()); + printf("\n"); + printf("Keys already in this wallet are NOT covered by the phrase -- they existed\n"); + printf("before it did. Keep your file backups as well.\n"); + fflush(stdout); + return 0; +} + +bool RestoreFromPhrase(const std::string& strMnemonic, + int nMinDepth, + RestoreProgressFn fnProgress, + void* pArg, + std::string& strErrorRet, + int& nRecoveredRet, + int& nDerivedRet) +{ + strErrorRet.clear(); + nRecoveredRet = 0; + nDerivedRet = 0; + + // Refuse before touching the wallet, not after. + // + // Without this, a restore on a node that has not synced derives the right + // keys, scans a chain that is not there, and reports "no transactions were + // found" -- which reads as "your coins are gone" to the one person least + // able to argue with it. Measured while testing: a copied data directory + // whose Berkeley DB environment failed to load left the node at height 0, + // the scan walked one block, and the message said nothing was found across + // 600 addresses. + if (!CanScanWalletTransactions(strErrorRet)) + return false; + + if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet)) + return false; + + // Derive forward in batches, scanning after each, until a whole batch turns + // up nothing. Every derived key is written to the wallet before the scan, + // because the scan asks the wallet what belongs to it. + int nTotalDerived = 0; + while (nTotalDerived < RESTORE_MAX) + { + // Count wallet transactions, not scan hits. + // + // ScanForWalletTransactions reports what it added *or updated*, and it + // walks the whole chain every time, so a transaction found in the first + // batch is reported again by every batch after it. Testing this against + // a real payment produced exactly +1 per batch and a loop that ran to + // the ceiling, deriving ten thousand addresses to rediscover one coin. + size_t nWalletBefore = 0; + CRITICAL_BLOCK(cs_mapWallet) + nWalletBefore = mapWallet.size(); + + std::string strDeriveError; + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDKey(nHDNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDNext++; + nTotalDerived++; + } + CWalletDB().WriteHDNext(nHDNext); + + ScanForWalletTransactions(pindexGenesisBlock); + + size_t nWalletAfter = 0; + CRITICAL_BLOCK(cs_mapWallet) + nWalletAfter = mapWallet.size(); + + if (fnProgress) + fnProgress(pArg, nTotalDerived, (int)nWalletAfter); + + // A whole batch with nothing in it means far enough -- unless the + // caller asked to look deeper anyway. + if (nWalletAfter == nWalletBefore && nTotalDerived >= nMinDepth) + break; + } + + // The pool is rebuilt from where derivation stopped, so the next address + // this wallet hands out is one the phrase can reproduce as well. + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + TopUpKeyPool(); + + CRITICAL_BLOCK(cs_mapWallet) + nRecoveredRet = (int)mapWallet.size(); + nDerivedRet = nTotalDerived; + return true; +} + +static void PrintRestoreProgress(void*, int nDerived, int nRecovered) +{ + printf(" %d addresses checked, %d transaction(s) recovered\n", nDerived, nRecovered); + fflush(stdout); +} + +int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth) +{ + AttachTerminal(); + + printf("Checking the phrase and the chain.\n"); + fflush(stdout); + + std::string strError; + int nRecovered = 0, nDerived = 0; + if (!RestoreFromPhrase(strMnemonic, nMinDepth, PrintRestoreProgress, NULL, + strError, nRecovered, nDerived)) + { + fprintf(stderr, "Cannot restore: %s\n", strError.c_str()); + fprintf(stderr, "Nothing was changed.\n"); + return 1; + } + + printf("\n"); + if (nRecovered > 0) + printf("Restored: %d transaction(s) across %d derived addresses.\n", + nRecovered, nDerived); + else + printf("No transactions were found for the first %d addresses of that phrase.\n" + "If you expected coins, check the words and the order.\n", nDerived); + printf("Start the node normally to see the balance.\n"); + fflush(stdout); + return 0; +} + +int CmdNewAddress() +{ + AttachTerminal(); + + std::vector vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + fprintf(stderr, "Could not get a key from the wallet.\n"); + return 1; + } + + printf("%s\n", PubKeyToAddress(vchPubKey).c_str()); + if (!HaveHDSeed()) + printf("(this wallet has no recovery phrase, so only a file backup covers this address)\n"); + fflush(stdout); + return 0; +} + +// Diagnostic: print the addresses a phrase produces, so "the restore found +// nothing" can be told apart from "that address was never derived". +int CmdShowDerived(int nCount) +{ + AttachTerminal(); + + if (!HaveHDSeed()) + { + fprintf(stderr, "This wallet has no recovery phrase installed.\n"); + return 1; + } + if (nCount <= 0) + nCount = 20; + + std::string strError; + for (int i = 0; i < nCount; i++) + { + CKey key; + if (!DeriveHDKey((unsigned int)i, key, strError)) + { + fprintf(stderr, "derivation failed at %d: %s\n", i, strError.c_str()); + return 1; + } + printf("%6d %s\n", i, PubKeyToAddress(key.GetPubKey()).c_str()); + } + fflush(stdout); + return 0; +} diff --git a/src/walletcmd.h b/src/walletcmd.h new file mode 100644 index 0000000..89834d8 --- /dev/null +++ b/src/walletcmd.h @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Recovery-phrase commands. Both run against a loaded wallet and then exit; +// neither starts the node. + +#ifndef BITFLASH_WALLETCMD_H +#define BITFLASH_WALLETCMD_H + +#include + +// Create a phrase for a wallet that does not have one, install the seed, and +// show the words once. Refuses if a phrase already exists. Returns 0 on success. +int CmdNewPhrase(); + +// Install the seed a phrase describes, derive forward looking for used +// addresses, and rescan the chain so their coins appear. Returns 0 on success. +int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth = 0); + +// Called once per batch with how many addresses have been derived so far and +// how many transactions the wallet now holds. +typedef void (*RestoreProgressFn)(void* pArg, int nDerived, int nRecovered); + +// The restore itself, shared by the command and the GUI so the two cannot +// drift apart. Returns false without touching the wallet if the chain is not +// loaded or the phrase is not valid. +bool RestoreFromPhrase(const std::string& strMnemonic, + int nMinDepth, + RestoreProgressFn fnProgress, + void* pArg, + std::string& strErrorRet, + int& nRecoveredRet, + int& nDerivedRet); + +// Take the next address from the key pool and print it. With a recovery phrase +// installed the address is derived, so the phrase can bring back whatever is +// paid to it. +int CmdNewAddress(); + +// Diagnostic: print the first nCount addresses this wallet's phrase derives. +int CmdShowDerived(int nCount); + +#endif -- cgit v1.2.3 From 5e37647023463cec4331dc48dc360ba60b22c888 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Mon, 3 Aug 2026 10:33:07 -0300 Subject: gui: read the Finish condition once, before the button that clears it (#118) Creating a recovery phrase from the window aborted with an ImGui assertion on the success path: Expression: (g.DisabledStackSize > 0) && "Calling EndDisabled() too many times!" File: imgui/imgui.cpp Line: 8435 The Finish button is wrapped in BeginDisabled/EndDisabled guarded by !g_phraseWrittenDown, and its own click handler sets g_phraseWrittenDown to false. So the pair was opened while the checkbox was ticked and closed after the handler had unticked it: BeginDisabled was never called, EndDisabled was, and ImGui asserted on the unmatched close. It fires on the only path that installs a seed -- tick the box, press Finish -- so every phrase created from the window has hit it since the feature shipped in 1.2.12. Three releases carried it because nothing tests the window. Reproduced on the published 1.2.14 binary and confirmed fixed on this build, driving the real window: Wallet Safety -> Create recovery phrase -> tick -> Finish. The old build raises the assertion; the fixed build closes the dialog, reports "This wallet has a recovery phrase", and the address changes to a derived one. The seed itself was already written before the assertion, so the wallet was never left half-installed -- and ImGui's IM_ASSERT_USER_ERROR_RET returns before touching its state, so answering Ignore was safe. Abort was not: it kills the process with no DBFlush. The restore dialog next door already does this correctly, copying the condition into a local first. This makes the create dialog match. --- src/gui.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index e800244..e3a8499 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1005,7 +1005,15 @@ static void DrawCreatePhraseDialog() ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown); ImGui::Spacing(); - if (!g_phraseWrittenDown) + // Read the condition once, before the button that changes it. + // + // Pressing Finish clears g_phraseWrittenDown, so testing it again after + // the click asked BeginDisabled and EndDisabled two different questions: + // the pair was opened while the checkbox was ticked and closed after the + // handler had unticked it. ImGui asserts on the unmatched EndDisabled -- + // on the success path, so every phrase created from the window hit it. + const bool fFinishDisabled = !g_phraseWrittenDown; + if (fFinishDisabled) ImGui::BeginDisabled(); if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f))) { @@ -1024,7 +1032,7 @@ static void DrawCreatePhraseDialog() g_showCreatePhrase = false; g_needRefresh = true; } - if (!g_phraseWrittenDown) + if (fFinishDisabled) ImGui::EndDisabled(); ImGui::SameLine(); -- cgit v1.2.3 From 57441faffb02f4f2eea37d31b29728de25f18bef Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:43:42 -0300 Subject: Add wallet recovery balance audit (#117) Co-authored-by: Gilcimar Duarte --- src/gui.cpp | 54 ++++++++++++++++++++++++++++++++-- src/main.cpp | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.h | 31 ++++++++++++++++++++ src/main_gui.cpp | 12 +++++++- src/selftest.cpp | 36 +++++++++++++++++++++++ src/walletcmd.cpp | 59 +++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 +++ 7 files changed, 280 insertions(+), 3 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index e3a8499..2335bf9 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -65,6 +65,8 @@ static std::string g_sendStatus; static char g_backupPath[512] = {}; static std::string g_backupStatus; static int64 g_lastWalletBackup = 0; +static WalletRecoveryAudit g_recoveryAudit; +static int64 g_recoveryAuditTime = 0; static bool g_walletSafetyLoaded = false; static char g_participantPool[256] = {}; static char g_poolName[128] = {}; @@ -184,6 +186,15 @@ static void LoadWalletSafetyState() g_walletSafetyLoaded = true; } +static void RefreshRecoveryAudit(bool fForce=false) +{ + int64 nNow = GetTime(); + if (!fForce && g_recoveryAuditTime != 0 && nNow - g_recoveryAuditTime < 5) + return; + g_recoveryAudit = GetWalletRecoveryAudit(); + g_recoveryAuditTime = nNow; +} + static int KeyPoolCount() { int n = 0; @@ -267,7 +278,11 @@ static void RefreshWallet() g_lastWalletRefresh = GetTime(); } -void MainFrameRepaint() { g_needRefresh = true; } +void MainFrameRepaint() +{ + g_needRefresh = true; + g_recoveryAuditTime = 0; +} // DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is // needed by builds that have no GUI at all. @@ -967,6 +982,7 @@ static void RestoreThread(std::string strPhrase) "Check the words and their order.", nDerived); } g_restoreRunning.store(false); + g_recoveryAuditTime = 0; g_needRefresh = true; } @@ -1021,6 +1037,7 @@ static void DrawCreatePhraseDialog() if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) { TopUpKeyPool(); + g_recoveryAuditTime = 0; g_phraseStatus = "Recovery phrase created. New addresses come from it."; } else @@ -1141,8 +1158,9 @@ static void DrawWalletSafetyDialog() { if (!g_showWalletSafety) return; LoadWalletSafetyState(); + RefreshRecoveryAudit(); - ImGui::SetNextWindowSize(ImVec2(650.0f, 390.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(650.0f, 450.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, @@ -1229,6 +1247,38 @@ static void DrawWalletSafetyDialog() ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "No recovery phrase. Only a file backup can rebuild this wallet."); + ImGui::Text("Phrase-backed spendable balance: %s BTF", + FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); + ImGui::Text("Wallet.dat-only spendable balance: %s BTF", + FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); + ImGui::Text("Phrase-backed immature mining rewards: %s BTF", + FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); + ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", + FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); + if (!g_recoveryAudit.fDeriveComplete) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + ImGui::TextWrapped("%s", g_recoveryAudit.strDeriveError.c_str()); + ImGui::PopStyleColor(); + } + if (g_recoveryAudit.nLegacyCredit > 0 || + g_recoveryAudit.nLegacyImmatureCredit > 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + ImGui::TextWrapped( + "Some coins are on keys the phrase does not reproduce. " + "Keep wallet.dat backups until that balance has been moved to a " + "phrase-backed address."); + ImGui::PopStyleColor(); + } + else if (HaveHDSeed() && + g_recoveryAudit.nRecoverableCredit + + g_recoveryAudit.nRecoverableImmatureCredit > 0) + { + ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), + "All known wallet balance is covered by the phrase."); + } + if (!HaveHDSeed()) { if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) diff --git a/src/main.cpp b/src/main.cpp index 1b09a1a..75647ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3660,6 +3660,93 @@ bool BitcoinMiner(int nThreadId) // +WalletRecoveryAudit GetWalletRecoveryAudit() +{ + WalletRecoveryAudit audit; + set > setDerivedPubKeys; + + CRITICAL_BLOCK(cs_keyPool) + { + audit.fHaveSeed = HaveHDSeed(); + audit.nDerivedKnown = nHDNext; + if (audit.fHaveSeed) + { + string strError; + for (unsigned int i = 0; i < nHDNext; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + { + audit.fDeriveComplete = false; + audit.strDeriveError = strprintf("derivation failed at index %u: %s", + i, strError.c_str()); + break; + } + setDerivedPubKeys.insert(key.GetPubKey()); + } + } + } + + CRITICAL_BLOCK(cs_mapWallet) + { + for (map::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) + { + CWalletTx& wtx = (*it).second; + if (!wtx.IsFinal() || wtx.fSpent) + continue; + bool fImmature = wtx.IsCoinBase() && wtx.GetBlocksToMaturity() > 0; + + bool fTxRecoverable = false; + bool fTxLegacy = false; + for (int i = 0; i < (int)wtx.vout.size(); i++) + { + const CTxOut& txout = wtx.vout[i]; + if (!txout.IsMine()) + continue; + + vector vchPubKey; + bool fRecoverable = false; + if (audit.fHaveSeed && ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) + fRecoverable = setDerivedPubKeys.count(vchPubKey) > 0; + + if (fRecoverable) + { + if (fImmature) + audit.nRecoverableImmatureCredit += txout.nValue; + else + audit.nRecoverableCredit += txout.nValue; + fTxRecoverable = true; + } + else + { + if (fImmature) + audit.nLegacyImmatureCredit += txout.nValue; + else + audit.nLegacyCredit += txout.nValue; + fTxLegacy = true; + } + } + + if (fTxRecoverable) + { + if (fImmature) + audit.nRecoverableImmatureTx++; + else + audit.nRecoverableTx++; + } + if (fTxLegacy) + { + if (fImmature) + audit.nLegacyImmatureTx++; + else + audit.nLegacyTx++; + } + } + } + + return audit; +} + int64 GetBalance() { int64 nStart, nEnd; diff --git a/src/main.h b/src/main.h index 768f1c7..4886f55 100644 --- a/src/main.h +++ b/src/main.h @@ -217,6 +217,37 @@ extern CCriticalSection cs_mapTransactions; bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv); bool SendMessages(CNode* pto); int64 GetBalance(); +struct WalletRecoveryAudit +{ + bool fHaveSeed; + bool fDeriveComplete; + unsigned int nDerivedKnown; + int nRecoverableTx; + int nLegacyTx; + int nRecoverableImmatureTx; + int nLegacyImmatureTx; + int64 nRecoverableCredit; + int64 nLegacyCredit; + int64 nRecoverableImmatureCredit; + int64 nLegacyImmatureCredit; + string strDeriveError; + + WalletRecoveryAudit() + { + fHaveSeed = false; + fDeriveComplete = true; + nDerivedKnown = 0; + nRecoverableTx = 0; + nLegacyTx = 0; + nRecoverableImmatureTx = 0; + nLegacyImmatureTx = 0; + nRecoverableCredit = 0; + nLegacyCredit = 0; + nRecoverableImmatureCredit = 0; + nLegacyImmatureCredit = 0; + } +}; +WalletRecoveryAudit GetWalletRecoveryAudit(); bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet); bool CommitTransactionSpent(const CWalletTx& wtxNew); bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 32fcdef..1b72813 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -93,7 +93,10 @@ static void PrintUsage() printf(" /restoredepth=N (with /restorephrase: derive at least N\n"); printf(" addresses before giving up)\n"); printf(" /showderived=N (list the first N addresses a phrase\n"); - printf(" produces, without installing it)\n"); + printf(" installed in this wallet derives)\n"); + printf(" /recoveryaudit (show how much spendable balance is\n"); + printf(" covered by the recovery phrase;\n"); + printf(" exits 2 when wallet.dat is still needed)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -385,6 +388,13 @@ int main(int argc, char* argv[]) return nRet; } + if (arg(argc,argv,"/recoveryaudit") || arg(argc,argv,"-recoveryaudit")) + { + int nRet = CmdRecoveryAudit(); + DBFlush(true); + return nRet; + } + if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) { int nRet = CmdNewPhrase(); diff --git a/src/selftest.cpp b/src/selftest.cpp index 67f96f8..df55bcd 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -336,6 +336,7 @@ static int RunWalletHDSelfTest() // The address the wallet was showing before any of this. It has to // survive under a name that says it is not covered by the phrase. + std::vector vchPreSeedKey = keyUser.GetPubKey(); std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), @@ -449,6 +450,41 @@ static int RunWalletHDSelfTest() } nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; + + // A recovery phrase is only useful if the wallet can tell the user + // what today's spendable balance would actually come back from it. + CKey keyAuditDerived; + if (!DeriveHDKey(0, keyAuditDerived, strError)) + throw std::runtime_error("audit derivation failed: " + strError); + + CWalletTx wtxLegacy; + wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); + CWalletTx wtxDerived; + wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); + CWalletTx wtxImmatureLegacy; + wtxImmatureLegacy.vin.push_back(CTxIn()); + wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); + + CRITICAL_BLOCK(cs_mapWallet) + { + mapWallet.clear(); + mapWallet[wtxLegacy.GetHash()] = wtxLegacy; + mapWallet[wtxDerived.GetHash()] = wtxDerived; + mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; + } + + WalletRecoveryAudit audit = GetWalletRecoveryAudit(); + nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; + nFail += Check(audit.nLegacyCredit == 5 * COIN, + "the recovery audit finds wallet.dat-only balance") ? 0 : 1; + nFail += Check(audit.nRecoverableCredit == 7 * COIN, + "the recovery audit finds phrase-backed balance") ? 0 : 1; + nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 1, + "the recovery audit counts legacy and phrase-backed transactions") ? 0 : 1; + nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, + "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; + nFail += Check(audit.nLegacyImmatureTx == 1, + "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; } catch (const std::exception& e) { diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6d5f32b..41b4ed2 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -272,6 +272,65 @@ int CmdShowDerived(int nCount) return 0; } +int CmdRecoveryAudit() +{ + AttachTerminal(); + + WalletRecoveryAudit audit = GetWalletRecoveryAudit(); + int64 nTotal = audit.nRecoverableCredit + audit.nLegacyCredit; + int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit; + + printf("Wallet recovery audit\n"); + printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); + if (audit.fHaveSeed) + printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); + if (!audit.fDeriveComplete) + printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); + printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); + printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", + FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); + printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n", + FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); + printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); + printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", + FormatMoney(audit.nRecoverableImmatureCredit).c_str(), + audit.nRecoverableImmatureTx); + printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n", + FormatMoney(audit.nLegacyImmatureCredit).c_str(), + audit.nLegacyImmatureTx); + + if (!audit.fHaveSeed) + { + printf("\n"); + printf("This wallet has no recovery phrase. A file backup is the only backup.\n"); + fflush(stdout); + return nTotal + nImmatureTotal > 0 ? 2 : 0; + } + if (!audit.fDeriveComplete) + { + printf("\n"); + printf("Warning: the audit could not derive every known phrase key, so coverage is incomplete.\n"); + fflush(stdout); + return 2; + } + if (audit.nLegacyCredit > 0 || audit.nLegacyImmatureCredit > 0) + { + printf("\n"); + printf("Warning: some coins are on keys the phrase does not reproduce.\n"); + printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n"); + fflush(stdout); + return 2; + } + + printf("\n"); + if (nTotal + nImmatureTotal > 0) + printf("All known wallet balance is covered by the recovery phrase.\n"); + else + printf("No wallet balance found yet.\n"); + fflush(stdout); + return 0; +} + // Spend, from the command line. // // SendMoney() has been in this tree since 0.1.0 and only the window ever called diff --git a/src/walletcmd.h b/src/walletcmd.h index 89834d8..796356b 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -40,4 +40,8 @@ int CmdNewAddress(); // Diagnostic: print the first nCount addresses this wallet's phrase derives. int CmdShowDerived(int nCount); +// Diagnostic: print whether the currently spendable wallet balance is covered +// by the installed recovery phrase or still depends on wallet.dat-only keys. +int CmdRecoveryAudit(); + #endif -- cgit v1.2.3 From aba252206bf39ac81e6627354062c28fae05b0aa Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:11:55 -0300 Subject: Activate BIP44 wallet derivation (#124) * Add BIP32 path derivation foundation * Add HD wallet schema metadata * Add provisional BIP44 coin type metadata * Activate BIP44 wallet derivation * wallet: scan BIP44 restore depth per branch * wallet: widen default BIP44 restore lookahead --------- Co-authored-by: Gilcimar Duarte --- src/bip32.cpp | 109 +++++++++++++++++++++---- src/bip32.h | 14 +++- src/db.cpp | 50 ++++++++++++ src/db.h | 27 ++++++- src/gui.cpp | 5 ++ src/main.cpp | 232 +++++++++++++++++++++++++++++++++++++++++++++++++---- src/main.h | 55 ++++++++++--- src/selftest.cpp | 115 +++++++++++++++++++++++--- src/test_bip32.cpp | 119 +++++++++++++++++++++++++++ src/walletcmd.cpp | 117 ++++++++++++++++++++++----- src/walletcmd.h | 9 +++ 11 files changed, 775 insertions(+), 77 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/bip32.cpp b/src/bip32.cpp index 6423974..6ba946a 100644 --- a/src/bip32.cpp +++ b/src/bip32.cpp @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen, len == 64; } +static bool PrivateKeyToCompressedPubKey(const std::vector& privateKey, + unsigned char out[33], + std::string& errorOut) +{ + if (!IsValidPrivateKey(privateKey)) + { + errorOut = "private key is invalid"; + return false; + } + + BIGNUM* bn = BN_bin2bn(&privateKey[0], 32, NULL); + BN_CTX* ctx = BN_CTX_new(); + EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); + EC_POINT* pub = group ? EC_POINT_new(group) : NULL; + + bool ok = false; + if (bn && ctx && group && pub && + EC_POINT_mul(group, pub, bn, NULL, NULL, ctx)) + { + size_t n = EC_POINT_point2oct(group, pub, POINT_CONVERSION_COMPRESSED, + out, 33, ctx); + ok = n == 33; + } + + BN_free(bn); + BN_CTX_free(ctx); + EC_POINT_free(pub); + EC_GROUP_free(group); + + if (!ok) + { + errorOut = "could not serialize parent public key"; + return false; + } + return true; +} + bool BIP39EntropyToMnemonic(const std::vector& entropy, std::string& mnemonicOut, std::string& errorOut) @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector& seed, return true; } -bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, - unsigned int childIndex, - BIP32PrivateNode& childOut, - std::string& errorOut) +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut) { childOut.privateKey.clear(); childOut.chainCode.clear(); @@ -297,20 +334,21 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, errorOut = "parent private key or chain code is invalid"; return false; } - if (childIndex >= 0x80000000U) + unsigned char data[37]; + if (childNumber & BIP32_HARDENED) { - errorOut = "child index must be non-hardened; hardening is applied here"; - return false; + data[0] = 0x00; + memcpy(data + 1, &parent.privateKey[0], 32); } - - const unsigned int hardened = childIndex | 0x80000000U; - unsigned char data[37]; - data[0] = 0x00; - memcpy(data + 1, &parent.privateKey[0], 32); - data[33] = (unsigned char)((hardened >> 24) & 0xff); - data[34] = (unsigned char)((hardened >> 16) & 0xff); - data[35] = (unsigned char)((hardened >> 8) & 0xff); - data[36] = (unsigned char)(hardened & 0xff); + else + { + if (!PrivateKeyToCompressedPubKey(parent.privateKey, data, errorOut)) + return false; + } + data[33] = (unsigned char)((childNumber >> 24) & 0xff); + data[34] = (unsigned char)((childNumber >> 16) & 0xff); + data[35] = (unsigned char)((childNumber >> 8) & 0xff); + data[36] = (unsigned char)(childNumber & 0xff); unsigned char I[64]; if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, return true; } +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + errorOut.clear(); + if (!IsValidPrivateKey(root.privateKey) || root.chainCode.size() != 32) + { + errorOut = "root private key or chain code is invalid"; + return false; + } + + BIP32PrivateNode cur = root; + for (size_t i = 0; i < path.size(); i++) + { + BIP32PrivateNode next; + if (!BIP32DeriveChild(cur, path[i], next, errorOut)) + return false; + cur = next; + } + nodeOut = cur; + return true; +} + +bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, + unsigned int childIndex, + BIP32PrivateNode& childOut, + std::string& errorOut) +{ + if (childIndex >= BIP32_HARDENED) + { + errorOut = "child index must be non-hardened; hardening is applied here"; + return false; + } + return BIP32DeriveChild(parent, childIndex | BIP32_HARDENED, childOut, errorOut); +} + } // namespace bitflash diff --git a/src/bip32.h b/src/bip32.h index a029444..e9862d5 100644 --- a/src/bip32.h +++ b/src/bip32.h @@ -5,7 +5,7 @@ // // This module is deliberately independent from wallet.dat and GUI code. It is // the auditable foundation: mnemonic generation/validation, seed derivation, -// and hardened private child derivation over secp256k1. +// and private child derivation over secp256k1. #ifndef BITFLASH_BIP32_H #define BITFLASH_BIP32_H @@ -16,6 +16,8 @@ namespace bitflash { +static const unsigned int BIP32_HARDENED = 0x80000000U; + struct BIP32PrivateNode { std::vector privateKey; // 32-byte scalar @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector& seed, BIP32PrivateNode& nodeOut, std::string& errorOut); +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut); + +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut); + bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, unsigned int childIndex, BIP32PrivateNode& childOut, diff --git a/src/db.cpp b/src/db.cpp index 294920b..b7e7b91 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -554,6 +554,7 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) // Whether wallet.dat actually carried a mining mode, as opposed to just the // ancient fGenerateBitcoins flag. See the reconciliation further down. bool fHaveStoredMineMode = false; + bool fHaveStoredHDCoinType = false; vchDefaultKeyRet.clear(); // Satoshi's "todo: shouldn't we catch exceptions" sat here since 2009, and @@ -651,6 +652,23 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { ssValue >> nHDNext; } + else if (strType == "hdschema") + { + ssValue >> nHDKeySchema; + } + else if (strType == "hdcointype") + { + ssValue >> nHDCoinType; + fHaveStoredHDCoinType = true; + } + else if (strType == "hdreceivenext") + { + ssValue >> nHDReceiveNext; + } + else if (strType == "hdchangenext") + { + ssValue >> nHDChangeNext; + } else if (strType == "pool") { int64 nIndex; @@ -707,6 +725,38 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) return false; } + if (HaveHDSeed()) + { + if (nHDKeySchema == HD_SCHEMA_NONE) + { + nHDKeySchema = HD_SCHEMA_LEGACY; + printf("LoadWallet: deterministic seed has no schema metadata; " + "treating it as legacy m/index'\n"); + } + else if (nHDKeySchema != HD_SCHEMA_LEGACY && + nHDKeySchema != HD_SCHEMA_BIP44) + { + printf("LoadWallet: deterministic wallet schema %d (%s) is not " + "supported by this build\n", + nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str()); + return false; + } + if (!fHaveStoredHDCoinType) + { + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " + "using provisional Bitflash coin type %u\n", nHDCoinType); + } + } + else + { + nHDKeySchema = HD_SCHEMA_NONE; + nHDNext = 0; + nHDReceiveNext = 0; + nHDChangeNext = 0; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + } + // fGenerateBitcoins and nMineMode only mean anything together, and a // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already // wrote fGenerateBitcoins when mining was toggled, years before this fork diff --git a/src/db.h b/src/db.h index 01b8583..90238bf 100644 --- a/src/db.h +++ b/src/db.h @@ -392,8 +392,11 @@ public: // twelve words are the user's to write down, and keeping a copy of them in // the file they are meant to protect defeats the point of having them. // - // "hdnext" is the next child index to derive, so the sequence a restore - // reproduces is the same one this wallet handed out. + // "hdschema" identifies the derivation layout. Wallets created before the + // field existed are inferred as HD_SCHEMA_LEGACY when a seed is present. + // "hdnext" is that legacy layout's next child index. The receive/change + // counters and coin type are reserved for the BIP44 layout that uses + // separate chains. bool ReadHDMaster(vector& vchMasterRet, vector& vchChainCodeRet) { vchMasterRet.clear(); @@ -413,6 +416,26 @@ public: return Write(string("hdnext"), nNext); } + bool WriteHDSchema(int nSchema) + { + return Write(string("hdschema"), nSchema); + } + + bool WriteHDCoinType(unsigned int nCoinType) + { + return Write(string("hdcointype"), nCoinType); + } + + bool WriteHDReceiveNext(unsigned int nNext) + { + return Write(string("hdreceivenext"), nNext); + } + + bool WriteHDChangeNext(unsigned int nNext) + { + return Write(string("hdchangenext"), nNext); + } + // Keys generated ahead of time and not yet handed out. The private key of // each is already stored under its own "key" record by AddKey; a "pool" // record only says that this one is still unspoken for. diff --git a/src/gui.cpp b/src/gui.cpp index 2335bf9..8f1d204 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1247,6 +1247,11 @@ static void DrawWalletSafetyDialog() ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "No recovery phrase. Only a file backup can rebuild this wallet."); + if (g_recoveryAudit.fHaveSeed) + { + ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); + ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); + } ImGui::Text("Phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); ImGui::Text("Wallet.dat-only spendable balance: %s BTF", diff --git a/src/main.cpp b/src/main.cpp index 75647ba..efece6c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -181,7 +181,7 @@ static bool AddKeyIfMissing(const CKey& key) return AddKey(key); } -static bool DeriveNextHDWalletKey(vector& vchPubKeyRet, string& strErrorRet) +static bool DeriveNextHDReceiveKey(vector& vchPubKeyRet, string& strErrorRet) { vchPubKeyRet.clear(); CRITICAL_BLOCK(cs_keyPool) @@ -193,7 +193,12 @@ static bool DeriveNextHDWalletKey(vector& vchPubKeyRet, string& s } CKey key; - if (!DeriveHDKey(nHDNext, key, strErrorRet)) + unsigned int nIndex = 0; + if (nHDKeySchema == HD_SCHEMA_BIP44) + nIndex = nHDReceiveNext; + else + nIndex = nHDNext; + if (!DeriveHDKey(nIndex, key, strErrorRet)) return false; if (!AddKeyIfMissing(key)) { @@ -201,14 +206,77 @@ static bool DeriveNextHDWalletKey(vector& vchPubKeyRet, string& s return false; } - unsigned int nNext = nHDNext + 1; - if (!CWalletDB().WriteHDNext(nNext)) + unsigned int nNext = nIndex + 1; + if (nHDKeySchema == HD_SCHEMA_BIP44) + { + if (!CWalletDB().WriteHDReceiveNext(nNext)) + { + strErrorRet = "could not advance the receive derivation counter"; + return false; + } + nHDReceiveNext = nNext; + } + else + { + if (!CWalletDB().WriteHDNext(nNext)) + { + strErrorRet = "could not advance the derivation counter"; + return false; + } + nHDNext = nNext; + } + + vchPubKeyRet = key.GetPubKey(); + } + return true; +} + +static bool DeriveNextHDChangeKey(vector& vchPubKeyRet, string& strErrorRet) +{ + vchPubKeyRet.clear(); + CRITICAL_BLOCK(cs_keyPool) + { + if (!HaveHDSeed()) { - strErrorRet = "could not advance the derivation counter"; + strErrorRet = "no seed"; return false; } - nHDNext = nNext; + CKey key; + if (nHDKeySchema == HD_SCHEMA_BIP44) + { + if (!DeriveHDChangeKey(nHDChangeNext, key, strErrorRet)) + return false; + if (!AddKeyIfMissing(key)) + { + strErrorRet = "could not write the derived change key to wallet.dat"; + return false; + } + unsigned int nNext = nHDChangeNext + 1; + if (!CWalletDB().WriteHDChangeNext(nNext)) + { + strErrorRet = "could not advance the change derivation counter"; + return false; + } + nHDChangeNext = nNext; + } + else + { + if (!DeriveHDKey(nHDNext, key, strErrorRet)) + return false; + if (!AddKeyIfMissing(key)) + { + strErrorRet = "could not write the derived change key to wallet.dat"; + return false; + } + unsigned int nNext = nHDNext + 1; + if (!CWalletDB().WriteHDNext(nNext)) + { + strErrorRet = "could not advance the derivation counter"; + return false; + } + nHDNext = nNext; + } vchPubKeyRet = key.GetPubKey(); } return true; @@ -229,6 +297,40 @@ map > mapKeyPool; vector vchHDMaster; vector vchHDChainCode; unsigned int nHDNext = 0; +int nHDKeySchema = HD_SCHEMA_NONE; +unsigned int nHDReceiveNext = 0; +unsigned int nHDChangeNext = 0; +unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + +string HDKeySchemaName(int nSchema) +{ + if (nSchema == HD_SCHEMA_LEGACY) + return "legacy-hd"; + if (nSchema == HD_SCHEMA_BIP44) + return "bip44"; + return "none"; +} + +std::vector HDLegacyPath(unsigned int nIndex) +{ + std::vector path; + path.push_back(nIndex | bitflash::BIP32_HARDENED); + return path; +} + +std::vector HDBIP44Path(unsigned int nCoinType, + unsigned int nAccount, + unsigned int nChain, + unsigned int nIndex) +{ + std::vector path; + path.push_back(HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED); + path.push_back(nCoinType | bitflash::BIP32_HARDENED); + path.push_back(nAccount | bitflash::BIP32_HARDENED); + path.push_back(nChain); + path.push_back(nIndex); + return path; +} static bool ClearKeyPoolRecords(string& strErrorRet) { @@ -254,13 +356,59 @@ bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) strErrorRet = "no seed"; return false; } + if (nIndex >= bitflash::BIP32_HARDENED) + { + strErrorRet = "child index must be non-hardened; hardening is applied here"; + return false; + } + + bitflash::BIP32PrivateNode parent; + parent.privateKey = vchHDMaster; + parent.chainCode = vchHDChainCode; + + bitflash::BIP32PrivateNode child; + std::vector path = nHDKeySchema == HD_SCHEMA_BIP44 + ? HDBIP44Path(nHDCoinType, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, nIndex) + : HDLegacyPath(nIndex); + if (!bitflash::BIP32DerivePath(parent, path, child, strErrorRet)) + return false; + + if (!keyRet.SetSecret(child.privateKey)) + { + strErrorRet = "derived scalar is not a usable key"; + return false; + } + return true; +} + +bool DeriveHDChangeKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) +{ + if (!HaveHDSeed()) + { + strErrorRet = "no seed"; + return false; + } + if (nIndex >= bitflash::BIP32_HARDENED) + { + strErrorRet = "child index must be non-hardened; hardening is applied here"; + return false; + } + + if (nHDKeySchema != HD_SCHEMA_BIP44) + return DeriveHDKey(nIndex, keyRet, strErrorRet); bitflash::BIP32PrivateNode parent; parent.privateKey = vchHDMaster; parent.chainCode = vchHDChainCode; bitflash::BIP32PrivateNode child; - if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) + if (!bitflash::BIP32DerivePath(parent, + HDBIP44Path(nHDCoinType, + HD_BIP44_ACCOUNT, + HD_BIP44_CHAIN_CHANGE, + nIndex), + child, + strErrorRet)) return false; if (!keyRet.SetSecret(child.privateKey)) @@ -307,20 +455,28 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) return false; if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || - !CWalletDB().WriteHDNext(1)) + !CWalletDB().WriteHDSchema(HD_SCHEMA_BIP44) || + !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL) || + !CWalletDB().WriteHDNext(0) || + !CWalletDB().WriteHDReceiveNext(1) || + !CWalletDB().WriteHDChangeNext(0)) { - strErrorRet = "could not write the seed to wallet.dat"; + strErrorRet = "could not write the seed metadata to wallet.dat"; return false; } vchHDMaster = master.privateKey; vchHDChainCode = master.chainCode; + nHDKeySchema = HD_SCHEMA_BIP44; + nHDReceiveNext = 1; + nHDChangeNext = 0; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; // Index 0 is spoken for before it is derived, so a failure below cannot // leave the pool free to hand it out as an ordinary key. The cost of // the counter being ahead of the derivation is one unused index; the // cost of the reverse is the default address and a pool key sharing a // derivation path. - nHDNext = 1; + nHDNext = 0; // Remember which address stops being the default, so it can be named // for what it is. Two entries reading "Your Address" -- one covered by @@ -372,15 +528,18 @@ void TopUpKeyPool() // without a seed uses anyway. It will not come back from the // phrase, and that is better than a node that cannot mine. string strError; - if (DeriveHDKey(nHDNext, key, strError)) + vector vchPubKey; + if (DeriveNextHDReceiveKey(vchPubKey, strError)) { - nHDNext++; - CWalletDB().WriteHDNext(nHDNext); + if (!CWalletDB().WritePool(nIndex, vchPubKey)) + return; + mapKeyPool[nIndex] = vchPubKey; + continue; } else { - printf("TopUpKeyPool() : derivation at index %u failed (%s), " - "falling back to a random key\n", nHDNext, strError.c_str()); + printf("TopUpKeyPool() : receive derivation failed (%s), " + "falling back to a random key\n", strError.c_str()); key.MakeNewKey(); } } @@ -3668,11 +3827,20 @@ WalletRecoveryAudit GetWalletRecoveryAudit() CRITICAL_BLOCK(cs_keyPool) { audit.fHaveSeed = HaveHDSeed(); + audit.nSchema = nHDKeySchema; audit.nDerivedKnown = nHDNext; + audit.nReceiveNext = nHDReceiveNext; + audit.nChangeNext = nHDChangeNext; + audit.nCoinType = nHDCoinType; + if (nHDKeySchema == HD_SCHEMA_BIP44) + audit.nDerivedKnown = nHDReceiveNext + nHDChangeNext + nHDNext; if (audit.fHaveSeed) { string strError; - for (unsigned int i = 0; i < nHDNext; i++) + unsigned int nReceiveDepth = nHDKeySchema == HD_SCHEMA_BIP44 + ? nHDReceiveNext + : nHDNext; + for (unsigned int i = 0; i < nReceiveDepth; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) @@ -3684,6 +3852,36 @@ WalletRecoveryAudit GetWalletRecoveryAudit() } setDerivedPubKeys.insert(key.GetPubKey()); } + if (nHDKeySchema == HD_SCHEMA_BIP44) + { + int nSavedSchema = nHDKeySchema; + for (unsigned int i = 0; i < nHDChangeNext; i++) + { + CKey key; + if (!DeriveHDChangeKey(i, key, strError)) + { + audit.fDeriveComplete = false; + audit.strDeriveError = strprintf("change derivation failed at index %u: %s", + i, strError.c_str()); + break; + } + setDerivedPubKeys.insert(key.GetPubKey()); + } + nHDKeySchema = HD_SCHEMA_LEGACY; + for (unsigned int i = 0; i < nHDNext; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + { + audit.fDeriveComplete = false; + audit.strDeriveError = strprintf("legacy compatibility derivation failed at index %u: %s", + i, strError.c_str()); + break; + } + setDerivedPubKeys.insert(key.GetPubKey()); + } + nHDKeySchema = nSavedSchema; + } } } @@ -3926,7 +4124,7 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in if (HaveHDSeed()) { string strError; - if (!DeriveNextHDWalletKey(vchPubKey, strError)) + if (!DeriveNextHDChangeKey(vchPubKey, strError)) { printf("CreateTransaction() : could not derive a phrase-backed change key: %s\n", strError.c_str()); diff --git a/src/main.h b/src/main.h index 4886f55..cca2cad 100644 --- a/src/main.h +++ b/src/main.h @@ -147,21 +147,45 @@ extern map > mapKeyPool; // --- Deterministic wallet (BIP32) ----------------------------------------- // -// When a seed is present the key pool is derived from it -- m/0'/n, hardened, -// n increasing -- instead of being made of random keys. That is what lets a -// recovery phrase bring a wallet back: the same twelve words reproduce the same -// keys in the same order. +// When a seed is present today, the key pool is derived from it by the legacy +// Bitflash path -- m/index', hardened, index increasing -- instead of being +// made of random keys. That is what lets a recovery phrase bring a wallet back: +// the same twelve words reproduce the same keys in the same order. // -// A wallet without a seed keeps working exactly as before. Nothing here -// migrates an existing wallet on its own: the seed is created only when the -// user asks for a phrase and confirms they have written it down, because a -// phrase generated silently is a backup nobody has. +// BIP44 uses a different path family and separate receive/change counters. +// The schema fields below let new wallets opt into that without making old +// m/index' coins disappear. +// +// A wallet without a seed keeps working exactly as before. Existing seeded +// wallets keep their recorded schema. A new phrase uses BIP44, but it is still +// created only when the user asks for a phrase and confirms they have written +// it down, because a phrase generated silently is a backup nobody has. // // Empty until a seed exists. extern vector vchHDMaster; // 32-byte master private key (IL) extern vector vchHDChainCode; // 32 bytes (IR) extern unsigned int nHDNext; // next child index to derive +static const int HD_SCHEMA_NONE = 0; +static const int HD_SCHEMA_LEGACY = 1; // m/index' +static const int HD_SCHEMA_BIP44 = 2; // m/44'/coin_type'/account'/change/index +static const unsigned int HD_BIP44_PURPOSE = 44; +static const unsigned int HD_BIP44_ACCOUNT = 0; +static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; +static const unsigned int HD_BIP44_CHAIN_CHANGE = 1; +// Provisional until Bitflash has an official SLIP-0044 assignment. +// Proposed registry row: 4346950 | BITFLASH | Bitflash. +static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950; +extern int nHDKeySchema; +extern unsigned int nHDReceiveNext; // BIP44 external chain +extern unsigned int nHDChangeNext; // BIP44 internal chain +extern unsigned int nHDCoinType; // BIP44 coin_type' inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } +string HDKeySchemaName(int nSchema); +std::vector HDLegacyPath(unsigned int nIndex); +std::vector HDBIP44Path(unsigned int nCoinType, + unsigned int nAccount, + unsigned int nChain, + unsigned int nIndex); // Install a seed derived from a mnemonic, replacing any existing one. The // default receiving key is derived immediately, so the next visible address is @@ -169,9 +193,12 @@ inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.siz // wallet untouched if the phrase is not valid. bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); -// Derive the child at nIndex and return it as a key. Used by the key pool and -// by restore, which needs to run ahead of the pool. +// Derive the receiving child at nIndex for the wallet's active schema and +// return it as a key. Legacy seeded wallets use m/index'. BIP44 wallets use +// m/44'/coin_type'/0'/0/index. Used by the key pool and restore, which needs +// to run ahead of the pool. bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); +bool DeriveHDChangeKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to // wallet.dat before it is offered to anybody. @@ -221,7 +248,11 @@ struct WalletRecoveryAudit { bool fHaveSeed; bool fDeriveComplete; + int nSchema; unsigned int nDerivedKnown; + unsigned int nReceiveNext; + unsigned int nChangeNext; + unsigned int nCoinType; int nRecoverableTx; int nLegacyTx; int nRecoverableImmatureTx; @@ -236,7 +267,11 @@ struct WalletRecoveryAudit { fHaveSeed = false; fDeriveComplete = true; + nSchema = HD_SCHEMA_NONE; nDerivedKnown = 0; + nReceiveNext = 0; + nChangeNext = 0; + nCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; nRecoverableTx = 0; nLegacyTx = 0; nRecoverableImmatureTx = 0; diff --git a/src/selftest.cpp b/src/selftest.cpp index df55bcd..aee2951 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -6,7 +6,9 @@ // and against a temporary data directory. #include "headers_core.h" +#include "bip32.h" #include "selftest.h" +#include "walletcmd.h" // Test results go to the terminal, not to debug.log. // @@ -342,18 +344,68 @@ static int RunWalletHDSelfTest() nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), "a valid phrase installs a seed") ? 0 : 1; nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; + nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, + "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; + nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; + nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, + "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; + std::vector vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + HD_BIP44_ACCOUNT, + HD_BIP44_CHAIN_RECEIVE, + 0); + nFail += Check(vBIP44Path.size() == 5 && + vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && + vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && + vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && + vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && + vBIP44Path[4] == 0, + "the BIP44 helper builds m/44'/coin_type'/account'/change/index") ? 0 : 1; int nPoolAfterSeed = 0; CRITICAL_BLOCK(cs_keyPool) nPoolAfterSeed = (int)mapKeyPool.size(); nFail += Check(nPoolAfterSeed == 0, "installing a phrase clears the old random key pool") ? 0 : 1; - nFail += Check(nHDNext == 1, - "installing a phrase reserves one derived default key") ? 0 : 1; + nFail += Check(nHDNext == 0, + "installing a BIP44 phrase leaves the legacy counter unused") ? 0 : 1; CKey keyFirstDerived; if (!DeriveHDKey(0, keyFirstDerived, strError)) throw std::runtime_error("default derivation failed: " + strError); + bitflash::BIP32PrivateNode hdParent; + hdParent.privateKey = vchHDMaster; + hdParent.chainCode = vchHDChainCode; + bitflash::BIP32PrivateNode hdBIP44ReceiveChild; + if (!bitflash::BIP32DerivePath(hdParent, + HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + HD_BIP44_ACCOUNT, + HD_BIP44_CHAIN_RECEIVE, + 0), + hdBIP44ReceiveChild, + strError)) + throw std::runtime_error("BIP44 receive path derivation failed: " + strError); + CKey keyBIP44ReceivePath; + if (!keyBIP44ReceivePath.SetSecret(hdBIP44ReceiveChild.privateKey)) + throw std::runtime_error("BIP44 receive path produced an unusable key"); + nFail += Check(keyBIP44ReceivePath.GetPubKey() == keyFirstDerived.GetPubKey(), + "the default HD key path is m/44'/coin_type'/0'/0/0") ? 0 : 1; + + bitflash::BIP32PrivateNode hdLegacyChild; + int nSchemaForLegacyCheck = nHDKeySchema; + nHDKeySchema = HD_SCHEMA_LEGACY; + CKey keyLegacyDerived; + if (!DeriveHDKey(0, keyLegacyDerived, strError)) + throw std::runtime_error("legacy derivation failed: " + strError); + nHDKeySchema = nSchemaForLegacyCheck; + if (!bitflash::BIP32DerivePath(hdParent, HDLegacyPath(0), hdLegacyChild, strError)) + throw std::runtime_error("legacy path derivation failed: " + strError); + CKey keyLegacyPath; + if (!keyLegacyPath.SetSecret(hdLegacyChild.privateKey)) + throw std::runtime_error("legacy path produced an unusable key"); + nFail += Check(keyLegacyPath.GetPubKey() == keyLegacyDerived.GetPubKey(), + "the legacy HD key path remains m/index'") ? 0 : 1; + std::vector vchDefaultKey; bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey); nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(), @@ -370,10 +422,16 @@ static int RunWalletHDSelfTest() "the pre-seed address is kept, named apart from the new one") ? 0 : 1; std::vector vchBefore = vchHDMaster; + int nSchemaBefore = nHDKeySchema; + unsigned int nCoinTypeBefore = nHDCoinType; nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), "an invalid phrase is refused") ? 0 : 1; nFail += Check(vchHDMaster == vchBefore, "a refused phrase leaves the existing seed alone") ? 0 : 1; + nFail += Check(nHDKeySchema == nSchemaBefore, + "a refused phrase leaves the derivation schema alone") ? 0 : 1; + nFail += Check(nHDCoinType == nCoinTypeBefore, + "a refused phrase leaves the BIP44 coin type alone") ? 0 : 1; // The property the whole feature exists for: the same words give back // the same keys, in the same order. @@ -416,18 +474,26 @@ static int RunWalletHDSelfTest() // With a seed installed the pool must be derived from it, and the // counter must move exactly once per key. SetHDSeedFromMnemonic(strPhraseA, strError); - unsigned int nNextBefore = nHDNext; + unsigned int nReceiveNextBefore = nHDReceiveNext; TopUpKeyPool(); int nPool = 0; CRITICAL_BLOCK(cs_keyPool) nPool = (int)mapKeyPool.size(); nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; - nFail += Check(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, - "the derivation counter advances once per pooled key") ? 0 : 1; + nFail += Check(nHDReceiveNext == nReceiveNextBefore + (unsigned int)KEYPOOL_SIZE, + "the BIP44 receive counter advances once per pooled key") ? 0 : 1; + nFail += Check(nHDChangeNext == 0, + "filling the receive pool leaves the BIP44 change counter alone") ? 0 : 1; + nFail += Check(!RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 400, 600, 600), + "BIP44 restore depth is not satisfied by receive plus legacy alone") ? 0 : 1; + nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 600, 600, 600), + "BIP44 restore depth is satisfied on each branch") ? 0 : 1; + nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_LEGACY, 0, 0, 600, 600), + "legacy restore depth still follows the legacy counter") ? 0 : 1; std::set derived; - for (unsigned int i = nNextBefore; i < nHDNext; i++) + for (unsigned int i = nReceiveNextBefore; i < nHDReceiveNext; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) @@ -456,11 +522,32 @@ static int RunWalletHDSelfTest() CKey keyAuditDerived; if (!DeriveHDKey(0, keyAuditDerived, strError)) throw std::runtime_error("audit derivation failed: " + strError); + CKey keyAuditChange; + if (!DeriveHDChangeKey(0, keyAuditChange, strError)) + throw std::runtime_error("audit change derivation failed: " + strError); + nFail += Check(keyAuditChange.GetPubKey() != keyAuditDerived.GetPubKey(), + "BIP44 receive and change chains derive different keys") ? 0 : 1; + if (!AddKey(keyAuditChange)) + throw std::runtime_error("could not store the audit change key"); + nHDChangeNext = 1; + int nSchemaForLegacyAudit = nHDKeySchema; + nHDKeySchema = HD_SCHEMA_LEGACY; + CKey keyAuditLegacyHD; + if (!DeriveHDKey(0, keyAuditLegacyHD, strError)) + throw std::runtime_error("audit legacy compatibility derivation failed: " + strError); + nHDKeySchema = nSchemaForLegacyAudit; + if (!AddKey(keyAuditLegacyHD)) + throw std::runtime_error("could not store the audit legacy compatibility key"); + nHDNext = 1; CWalletTx wtxLegacy; wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); CWalletTx wtxDerived; wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); + CWalletTx wtxChange; + wtxChange.vout.push_back(CTxOut(13 * COIN, CScript() << keyAuditChange.GetPubKey() << OP_CHECKSIG)); + CWalletTx wtxLegacyHD; + wtxLegacyHD.vout.push_back(CTxOut(17 * COIN, CScript() << keyAuditLegacyHD.GetPubKey() << OP_CHECKSIG)); CWalletTx wtxImmatureLegacy; wtxImmatureLegacy.vin.push_back(CTxIn()); wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); @@ -470,17 +557,25 @@ static int RunWalletHDSelfTest() mapWallet.clear(); mapWallet[wtxLegacy.GetHash()] = wtxLegacy; mapWallet[wtxDerived.GetHash()] = wtxDerived; + mapWallet[wtxChange.GetHash()] = wtxChange; + mapWallet[wtxLegacyHD.GetHash()] = wtxLegacyHD; mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; } WalletRecoveryAudit audit = GetWalletRecoveryAudit(); nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; + nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, + "the recovery audit reports the derivation schema") ? 0 : 1; + nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + "the recovery audit reports the BIP44 coin type") ? 0 : 1; + nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, + "the recovery audit reports receive/change counters") ? 0 : 1; nFail += Check(audit.nLegacyCredit == 5 * COIN, "the recovery audit finds wallet.dat-only balance") ? 0 : 1; - nFail += Check(audit.nRecoverableCredit == 7 * COIN, - "the recovery audit finds phrase-backed balance") ? 0 : 1; - nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 1, - "the recovery audit counts legacy and phrase-backed transactions") ? 0 : 1; + nFail += Check(audit.nRecoverableCredit == 37 * COIN, + "the recovery audit finds BIP44 and legacy-HD phrase balance") ? 0 : 1; + nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 3, + "the recovery audit counts wallet.dat-only and phrase-backed transactions") ? 0 : 1; nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; nFail += Check(audit.nLegacyImmatureTx == 1, diff --git a/src/test_bip32.cpp b/src/test_bip32.cpp index 610a651..6c11510 100644 --- a/src/test_bip32.cpp +++ b/src/test_bip32.cpp @@ -5,6 +5,8 @@ #include "bip32.h" +#include + #include #include #include @@ -57,6 +59,76 @@ static std::string ToHex(const std::vector& v) return out; } +static bool DecodeBase58Check(const std::string& str, std::vector& out) +{ + static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + out.clear(); + + std::vector b256(str.size() * 733 / 1000 + 1); + for (char c : str) + { + const char* p = strchr(pszBase58, c); + if (!p) + return false; + int carry = (int)(p - pszBase58); + for (std::vector::reverse_iterator it = b256.rbegin(); + it != b256.rend(); ++it) + { + carry += 58 * (*it); + *it = (unsigned char)(carry & 0xff); + carry >>= 8; + } + if (carry != 0) + return false; + } + + size_t zeros = 0; + while (zeros < str.size() && str[zeros] == '1') + zeros++; + std::vector::iterator it = b256.begin(); + while (it != b256.end() && *it == 0) + ++it; + + std::vector full; + full.assign(zeros, 0); + while (it != b256.end()) + full.push_back(*it++); + if (full.size() < 4) + return false; + + std::vector payload(full.begin(), full.end() - 4); + unsigned char h1[SHA256_DIGEST_LENGTH]; + unsigned char h2[SHA256_DIGEST_LENGTH]; + SHA256(&payload[0], payload.size(), h1); + SHA256(h1, sizeof(h1), h2); + if (memcmp(h2, &full[full.size() - 4], 4) != 0) + return false; + + out = payload; + return true; +} + +static bool DecodeXPrvNode(const std::string& xprv, BIP32PrivateNode& nodeOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + + std::vector payload; + if (!DecodeBase58Check(xprv, payload)) + return false; + if (payload.size() != 78) + return false; + if (payload[0] != 0x04 || payload[1] != 0x88 || + payload[2] != 0xad || payload[3] != 0xe4) + return false; + if (payload[45] != 0x00) + return false; + + nodeOut.chainCode.assign(payload.begin() + 13, payload.begin() + 45); + nodeOut.privateKey.assign(payload.begin() + 46, payload.end()); + return true; +} + int main() { std::string err; @@ -124,6 +196,53 @@ int main() "pre-hardened index rejected"); } + printf("bip32_paths\n"); + { + std::vector seed = FromHex("000102030405060708090a0b0c0d0e0f"); + BIP32PrivateNode master; + CHECK(BIP32MasterFromSeed(seed, master, err), "path test master derives"); + + struct PathVector + { + const char* name; + std::vector path; + const char* xprv; + }; + + std::vector vectors; + vectors.push_back({"m", + {}, + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}); + vectors.push_back({"m/0'", + {0 | BIP32_HARDENED}, + "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"}); + vectors.push_back({"m/0'/1", + {0 | BIP32_HARDENED, 1}, + "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"}); + vectors.push_back({"m/0'/1/2'", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED}, + "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"}); + vectors.push_back({"m/0'/1/2'/2", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2}, + "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"}); + vectors.push_back({"m/0'/1/2'/2/1000000000", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2, 1000000000}, + "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"}); + + for (const PathVector& v : vectors) + { + BIP32PrivateNode actual; + BIP32PrivateNode expected; + std::string label = std::string("derives ") + v.name; + CHECK(BIP32DerivePath(master, v.path, actual, err), label.c_str()); + label = std::string("decodes vector ") + v.name; + CHECK(DecodeXPrvNode(v.xprv, expected), label.c_str()); + label = std::string("matches vector ") + v.name; + CHECK(actual.privateKey == expected.privateKey && + actual.chainCode == expected.chainCode, label.c_str()); + } + } + printf("\n%s (%d failures)\n", g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); return g_fail == 0 ? 0 : 1; diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 41b4ed2..b449df9 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -25,16 +25,34 @@ // would restore a wallet that looks empty. static const int RESTORE_BATCH = 100; static const int RESTORE_MAX = 10000; -// The wallet may have a full unused key pool in front of a change address -// created while spending pre-phrase coins. Looking only one empty batch ahead -// can stop just before that change output. -static const int RESTORE_MIN_SCAN = KEYPOOL_SIZE + RESTORE_BATCH; - // Quiet batches required before giving up. Three, because the wallet's own // bookkeeping can leave a gap of two hundred used-nothing indices between one // used address and the next -- see the note at the stop condition. One was not // enough and cost a real balance in testing. static const int RESTORE_QUIET_BATCHES = 3; +// The wallet may have restored once, scanned a few quiet batches, and then +// spent after that. Change starts from wherever that restore stopped, so the +// next real change output can be just beyond the old quiet window. The default +// restore has to look far enough for that ordinary "restore, then spend, then +// restore again" path; explicit -restoredepth can still go deeper. +static const int RESTORE_MIN_SCAN = + KEYPOOL_SIZE + (RESTORE_QUIET_BATCHES + 1) * RESTORE_BATCH; + +bool RestoreScanReachedDepth(int nSchema, + unsigned int nReceiveNext, + unsigned int nChangeNext, + unsigned int nLegacyNext, + int nStopDepth) +{ + if (nStopDepth <= 0) + return true; + unsigned int nDepth = (unsigned int)nStopDepth; + if (nSchema == HD_SCHEMA_BIP44) + return nReceiveNext >= nDepth && + nChangeNext >= nDepth && + nLegacyNext >= nDepth; + return nLegacyNext >= nDepth; +} int CmdNewPhrase() { @@ -114,7 +132,9 @@ bool RestoreFromPhrase(const std::string& strMnemonic, // Derive forward in batches, scanning after each, until a whole batch turns // up nothing. Every derived key is written to the wallet before the scan, // because the scan asks the wallet what belongs to it. - int nTotalDerived = (int)nHDNext; + int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44 + ? (int)(nHDReceiveNext + nHDChangeNext) + : (int)nHDNext; int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); int nQuietBatches = 0; while (nTotalDerived < RESTORE_MAX) @@ -131,17 +151,61 @@ bool RestoreFromPhrase(const std::string& strMnemonic, nWalletBefore = mapWallet.size(); std::string strDeriveError; - for (int i = 0; i < RESTORE_BATCH; i++) + if (nHDKeySchema == HD_SCHEMA_BIP44) + { + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDKey(nHDReceiveNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDReceiveNext++; + nTotalDerived++; + } + CWalletDB().WriteHDReceiveNext(nHDReceiveNext); + + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDChangeKey(nHDChangeNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDChangeNext++; + nTotalDerived++; + } + CWalletDB().WriteHDChangeNext(nHDChangeNext); + + int nSavedSchema = nHDKeySchema; + nHDKeySchema = HD_SCHEMA_LEGACY; + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDKey(nHDNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDNext++; + nTotalDerived++; + } + nHDKeySchema = nSavedSchema; + CWalletDB().WriteHDNext(nHDNext); + } + else { - CKey key; - if (!DeriveHDKey(nHDNext, key, strDeriveError)) - break; - if (!AddKey(key)) - break; - nHDNext++; - nTotalDerived++; + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDKey(nHDNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDNext++; + nTotalDerived++; + } + CWalletDB().WriteHDNext(nHDNext); } - CWalletDB().WriteHDNext(nHDNext); ScanForWalletTransactions(pindexGenesisBlock); @@ -157,11 +221,11 @@ bool RestoreFromPhrase(const std::string& strMnemonic, } // Stopping needs more than one quiet batch, because this wallet digs - // gaps in its own derivation. A restore leaves nHDNext at the depth it - // scanned, the key pool then derives KEYPOOL_SIZE more, and change - // takes the index after that -- so the address used *after* a restore - // can sit two hundred indices past the last one used before it, with - // nothing in between. + // gaps in its own derivation. A restore leaves the receive counter at + // the depth it scanned, the key pool then derives KEYPOOL_SIZE more, + // and a later spend can use change after that -- so the address used + // *after* a restore can sit two hundred indices past the last one used + // before it, with nothing in between. // // Measured, on a real wallet with real coin: restore, spend once, and // the coins land at indices 201 and 302. Restoring again with the old @@ -171,7 +235,12 @@ bool RestoreFromPhrase(const std::string& strMnemonic, nQuietBatches++; else nQuietBatches = 0; - if (nQuietBatches >= RESTORE_QUIET_BATCHES && nTotalDerived >= nStopDepth) + if (nQuietBatches >= RESTORE_QUIET_BATCHES && + RestoreScanReachedDepth(nHDKeySchema, + nHDReceiveNext, + nHDChangeNext, + nHDNext, + nStopDepth)) break; } @@ -283,7 +352,13 @@ int CmdRecoveryAudit() printf("Wallet recovery audit\n"); printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); if (audit.fHaveSeed) + { + printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); + printf(" BIP44 coin type: %u (provisional BITFLASH)\n", audit.nCoinType); printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); + printf(" receive/change counters: %u/%u\n", + audit.nReceiveNext, audit.nChangeNext); + } if (!audit.fDeriveComplete) printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); diff --git a/src/walletcmd.h b/src/walletcmd.h index 69874e4..32d3eef 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -32,6 +32,15 @@ bool RestoreFromPhrase(const std::string& strMnemonic, int& nRecoveredRet, int& nDerivedRet); +// Restore scans stop only after the requested depth is reached. BIP44 has more +// than one branch, so the depth has to be satisfied per branch, not by summing +// receive + change + compatibility keys. +bool RestoreScanReachedDepth(int nSchema, + unsigned int nReceiveNext, + unsigned int nChangeNext, + unsigned int nLegacyNext, + int nStopDepth); + // Take the next address from the key pool and print it. With a recovery phrase // installed the address is derived, so the phrase can bring back whatever is // paid to it. -- cgit v1.2.3 From dfbf7a91caa4a85da7ce10fa296cfa6d2ead5d0c Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 11 Aug 2026 09:57:29 -0300 Subject: wallet: SLIP-0044 4346950 is official, drop 'provisional' naming The coin type 4346950 (0x425446 = BTF) was merged into the SLIP-0044 registry on 2026-08-07. Rename HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL to HD_BIP44_COIN_TYPE_BITFLASH and update comments, logs and UI strings. Value is unchanged, so derivation paths and addresses are identical. --- src/db.cpp | 6 +++--- src/gui.cpp | 2 +- src/main.cpp | 6 +++--- src/main.h | 8 ++++---- src/selftest.cpp | 12 ++++++------ src/walletcmd.cpp | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/db.cpp b/src/db.cpp index 244d5a7..9f62964 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -880,9 +880,9 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) } if (!fHaveStoredHDCoinType) { - nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " - "using provisional Bitflash coin type %u\n", nHDCoinType); + "using Bitflash coin type %u\n", nHDCoinType); } } else @@ -891,7 +891,7 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) nHDNext = 0; nHDReceiveNext = 0; nHDChangeNext = 0; - nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; } // fGenerateBitcoins and nMineMode only mean anything together, and a diff --git a/src/gui.cpp b/src/gui.cpp index 8f1d204..8dcb854 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1250,7 +1250,7 @@ static void DrawWalletSafetyDialog() if (g_recoveryAudit.fHaveSeed) { ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); - ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); + ImGui::Text("BIP44 coin type: %u (SLIP-0044 BITFLASH)", g_recoveryAudit.nCoinType); } ImGui::Text("Phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); diff --git a/src/main.cpp b/src/main.cpp index 2eb0e3a..f83164f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -642,7 +642,7 @@ unsigned int nHDNext = 0; int nHDKeySchema = HD_SCHEMA_NONE; unsigned int nHDReceiveNext = 0; unsigned int nHDChangeNext = 0; -unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; +unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; string HDKeySchemaName(int nSchema) { @@ -798,7 +798,7 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || !CWalletDB().WriteHDSchema(HD_SCHEMA_BIP44) || - !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL) || + !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH) || !CWalletDB().WriteHDNext(0) || !CWalletDB().WriteHDReceiveNext(1) || !CWalletDB().WriteHDChangeNext(0)) @@ -811,7 +811,7 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) nHDKeySchema = HD_SCHEMA_BIP44; nHDReceiveNext = 1; nHDChangeNext = 0; - nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; // Index 0 is spoken for before it is derived, so a failure below cannot // leave the pool free to hand it out as an ordinary key. The cost of diff --git a/src/main.h b/src/main.h index eafbf16..b389f1d 100644 --- a/src/main.h +++ b/src/main.h @@ -216,9 +216,9 @@ static const unsigned int HD_BIP44_PURPOSE = 44; static const unsigned int HD_BIP44_ACCOUNT = 0; static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; static const unsigned int HD_BIP44_CHAIN_CHANGE = 1; -// Provisional until Bitflash has an official SLIP-0044 assignment. -// Proposed registry row: 4346950 | BITFLASH | Bitflash. -static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950; +// Official SLIP-0044 assignment (registered 2026-08-07): +// 4346950 | BITFLASH | Bitflash (4346950 == 0x425446 == "BTF") +static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH = 4346950; extern int nHDKeySchema; extern unsigned int nHDReceiveNext; // BIP44 external chain extern unsigned int nHDChangeNext; // BIP44 internal chain @@ -316,7 +316,7 @@ struct WalletRecoveryAudit nDerivedKnown = 0; nReceiveNext = 0; nChangeNext = 0; - nCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; + nCoinType = HD_BIP44_COIN_TYPE_BITFLASH; nRecoverableTx = 0; nLegacyTx = 0; nRecoverableImmatureTx = 0; diff --git a/src/selftest.cpp b/src/selftest.cpp index fadc959..69a3596 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -350,17 +350,17 @@ static int RunWalletHDSelfTest() nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; - nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, - "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; + nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH, + "a new recovery phrase records the SLIP-0044 BIP44 coin type") ? 0 : 1; nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; - std::vector vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + std::vector vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, 0); nFail += Check(vBIP44Path.size() == 5 && vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && - vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && + vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH | bitflash::BIP32_HARDENED) && vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && vBIP44Path[4] == 0, @@ -382,7 +382,7 @@ static int RunWalletHDSelfTest() hdParent.chainCode = vchHDChainCode; bitflash::BIP32PrivateNode hdBIP44ReceiveChild; if (!bitflash::BIP32DerivePath(hdParent, - HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, 0), @@ -570,7 +570,7 @@ static int RunWalletHDSelfTest() nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, "the recovery audit reports the derivation schema") ? 0 : 1; - nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, + nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH, "the recovery audit reports the BIP44 coin type") ? 0 : 1; nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, "the recovery audit reports receive/change counters") ? 0 : 1; diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 06f35a5..0ea2251 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -354,7 +354,7 @@ int CmdRecoveryAudit() if (audit.fHaveSeed) { printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); - printf(" BIP44 coin type: %u (provisional BITFLASH)\n", audit.nCoinType); + printf(" BIP44 coin type: %u (SLIP-0044 BITFLASH)\n", audit.nCoinType); printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); printf(" receive/change counters: %u/%u\n", audit.nReceiveNext, audit.nChangeNext); -- cgit v1.2.3 From 477d57b9a01102b6f79ad9b10b2dcfd280faa1cb Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 16:26:34 -0300 Subject: gui: offer to convert a Berkeley DB wallet to SQLite on first run A first-run dialog for an existing wallet.dat with no backend choice yet: "Convert to SQLite" exports the wallet and records the sqlite marker so the next start opens it; "Keep Berkeley DB" records that choice so the offer does not return; "Decide later" leaves no marker and asks again. wallet.dat is never touched, and the change takes effect on restart -- matching the restart-to-apply model. The conversion itself is DoConvertWalletToSQLite(), a headless-testable function the button calls: it reuses the export path (extracted as ExportActiveWalletToSQLite, shared with -walletsqliteexport, which keeps its own specific messages), refuses if already on SQLite or if a wallet.sqlite exists, and writes the marker only after a verified export. New wallet-convert self-test proves the action end to end: build a Berkeley DB wallet, convert, and confirm wallet.sqlite + the marker are written, wallet.dat is left untouched, a second convert refuses, and a flagless restart opens the converted SQLite wallet with its phrase intact. All wallet suites pass. The modal itself is thin UI over this tested action. --- src/gui.cpp | 90 ++++++++++++++++++++++++++++++++++++++++ src/makefile.mingw | 1 + src/selftest.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 119 +++++++++++++++++++++++++++++++++++++---------------- src/walletcmd.h | 9 ++++ 5 files changed, 290 insertions(+), 37 deletions(-) (limited to 'src/gui.cpp') 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() ImGui::End(); } +// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the +// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite +// and records the backend marker (wallet.dat is left untouched, so a restart +// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB" +// just records that choice so the offer does not return; "Decide later" leaves +// no marker and asks again next start. +static bool g_showBackendWizard = false; +static std::string g_wizardStatus; +static bool g_wizardConverted = false; + +static void DrawBackendWizardDialog() +{ + if (!g_showBackendWizard) return; + ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Wallet Storage", &g_showBackendWizard, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + if (g_wizardConverted) + { + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite."); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::Spacing(); + ImGui::TextWrapped( + "Restart Bitflash to start using the SQLite wallet. Your Berkeley " + "DB wallet.dat is kept untouched as a fallback."); + ImGui::Spacing(); + if (ImGui::Button("Close", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + } + else + { + ImGui::TextWrapped( + "Bitflash can store your wallet in a new SQLite format: a single " + "file that is easier to back up and does not depend on the " + "database/ directory beside it."); + ImGui::Spacing(); + ImGui::TextWrapped( + "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it " + "is -- nothing is deleted, and you can go back to it at any time. " + "The change takes effect after you restart."); + ImGui::Spacing(); + if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f))) + { + int nCopied = 0; + std::string strError; + if (DoConvertWalletToSQLite(nCopied, strError)) + { + g_wizardStatus = + strprintf("%d record(s) copied to wallet.sqlite.", nCopied); + g_wizardConverted = true; + } + else + g_wizardStatus = "Conversion failed: " + strError; + } + ImGui::SameLine(); + if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f))) + { + WriteWalletBackendMarker("bdb"); + g_showBackendWizard = false; + } + ImGui::SameLine(); + if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + + if (!g_wizardStatus.empty()) + { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f)); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::PopStyleColor(); + } + } + } + ImGui::End(); +} + // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- @@ -1457,6 +1535,17 @@ int RunGUI(int argc, char* argv[]) g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + // First run on a Berkeley DB wallet with no recorded choice yet: offer to + // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists, + // or once a choice has been recorded in the backend marker. + if (!WalletSQLiteRuntimeActive() && + FileExists((GetAppDir() + "/wallet.dat").c_str()) && + !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) && + ReadWalletBackendMarker().empty()) + { + g_showBackendWizard = true; + } + int frame = 0; while (!glfwWindowShouldClose(window) && !fShutdown) { @@ -1480,6 +1569,7 @@ int RunGUI(int argc, char* argv[]) DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); + DrawBackendWizardDialog(); DrawStatusBar(); ImGui::Render(); diff --git a/src/makefile.mingw b/src/makefile.mingw index 06c9211..55f186b 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -114,6 +114,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=wallet-sqlite-encrypt ./bitflash.exe -selftest=wallet-backend-default + ./bitflash.exe -selftest=wallet-convert ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum diff --git a/src/selftest.cpp b/src/selftest.cpp index 1960003..a9c73fe 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2664,6 +2664,110 @@ static int RunWalletBackendDefaultSelfTest() return nFail == 0 ? 0 : 1; } +// The wizard's conversion action, headless: build a Berkeley DB wallet, run +// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a +// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then +// that a flagless restart opens the converted SQLite wallet. +static int RunWalletConvertSelfTest() +{ + fflush(stdout); + printf("wallet-convert self-test\n"); + + std::string tmp, cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + nFail += Check(SetHDSeedFromMnemonic( + "abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon about", strError), + "a phrase can be installed before conversion") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before conversion") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), + vchDefaultPrivKey.end()); + + // The conversion action the GUI button invokes. + int nCopied = 0; + string strConvertError; + bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); + nFail += Check(fConverted && nCopied > 0, + "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; + nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && + FileContainsText(tmp + "/wallet-backend", "sqlite"), + "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; + nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), + "wallet.dat is left untouched by the conversion") ? 0 : 1; + + // Idempotence guard: a second conversion refuses rather than overwrite. + int nCopied2 = 0; + string strSecondError; + nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), + "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; + + DBFlush(true); + + // A flagless restart now opens the converted SQLite wallet (the marker + // routes it) and finds the same recovery phrase. + string strAudit = tmp + "/converted-recovery-audit.txt"; + vector vAuditArgs; + vAuditArgs.push_back("-datadir=" + tmp); + vAuditArgs.push_back("-nomanagedtor"); + vAuditArgs.push_back("-nogui"); + vAuditArgs.push_back("-recoveryaudit"); + int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); + nFail += Check(nAuditRet == 0 && + FileContainsText(strAudit, "recovery phrase: present"), + "a flagless restart opens the converted SQLite wallet") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunWalletPortabilitySelfTest() { fflush(stdout); @@ -3398,6 +3502,8 @@ int RunSelfTest(const std::string& name) return RunWalletSQLiteEncryptSelfTest(); if (name == "wallet-backend-default") return RunWalletBackendDefaultSelfTest(); + if (name == "wallet-convert") + return RunWalletConvertSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -3414,6 +3520,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - 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-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + 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"); return 1; } 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: } // namespace -int CmdWalletSQLiteExport(const std::string& strDest) +// Export the currently-loaded wallet to a SQLite file. Shared by the +// -walletsqliteexport command and the GUI conversion wizard: opens the +// destination, copies every record inside one transaction, checkpoints, and +// verifies the stored count matches. Returns false with strErrorRet set on any +// failure and leaves no partial file behind; the source wallet is only read. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet) { - AttachTerminal(); - + nCopiedRet = 0; if (strDest.empty()) { - fprintf(stderr, "Missing SQLite wallet export path.\n"); - return 1; + strErrorRet = "no SQLite export path was given"; + return false; } if (FileExists(strDest.c_str())) { - fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", - strDest.c_str()); - return 1; + strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str()); + return false; } - string strError; CWalletDBSQLite db; - if (!db.Open(strDest, strError)) + if (!db.Open(strDest, strErrorRet)) { RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.BeginTransaction(strError)) + if (!db.BeginTransaction(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - int nCopied = 0; - CWalletSQLiteExportVisitor visitor(db, nCopied); - if (!ScanWalletRecords(visitor, strError)) + CWalletSQLiteExportVisitor visitor(db, nCopiedRet); + if (!ScanWalletRecords(visitor, strErrorRet)) { string strRollbackError; db.RollbackTransaction(strRollbackError); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.CommitTransaction(strError)) + if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.Checkpoint(strError)) + + int nCount = 0; + if (!db.CountRecords(nCount, strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - - int nCount = 0; - if (!db.CountRecords(nCount, strError)) + if (nCount != nCopiedRet) { + strErrorRet = strprintf("export count mismatch: copied %d, stored %d", + nCopiedRet, nCount); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", - strError.c_str()); + return false; + } + db.Close(); + return true; +} + +// Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export +// wallet.dat to /wallet.sqlite, then record the backend marker so the +// next start opens SQLite. wallet.dat is never modified -- it stays as the +// fallback. Refuses if already on SQLite or if a wallet.sqlite already exists. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet) +{ + nCopiedRet = 0; + if (WalletSQLiteRuntimeActive()) + { + strErrorRet = "this wallet is already on the SQLite backend"; + return false; + } + string strDest = GetAppDir() + "/wallet.sqlite"; + if (FileExists(strDest.c_str())) + { + strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str()); + return false; + } + if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet)) + return false; + if (!WriteWalletBackendMarker("sqlite")) + { + strErrorRet = "the wallet was exported but the backend choice could not be saved"; + return false; + } + return true; +} + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + // Keep the command's own specific messages for the two conditions the + // shared helper folds into a generic error, so scripts and the tests that + // read this output see the same wording as before. + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); return 1; } - if (nCount != nCopied) + if (FileExists(strDest.c_str())) { - db.Close(); - RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", - nCopied, nCount); + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + int nCopied = 0; + string strError; + if (!ExportActiveWalletToSQLite(strDest, nCopied, strError)) + { + fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str()); return 1; } 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(); // not change which backend the runtime wallet opens. int CmdWalletSQLiteExport(const std::string& strDest); +// Export the currently-loaded wallet to a SQLite file (shared by the command +// above and the GUI wizard). Returns false with an error on any failure. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet); + +// Convert the running Berkeley DB wallet to SQLite and record the backend +// marker so the next start opens it. wallet.dat is left untouched as fallback. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet); + // Diagnostic/migration staging: compare a SQLite wallet export with the current // wallet.dat raw records. Prints counts only, never record keys or values. int CmdWalletSQLiteVerify(const std::string& strPath); -- cgit v1.2.3 From 8a47568dcfec8869bc08c555f0070be2d7ddddce Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 09:23:01 -0300 Subject: gui: add a "Restart Now" button to the conversion wizard After converting to SQLite the wizard said "restart to apply" but offered no way to do it, so a user could close the dialog and keep mining on the old Berkeley DB wallet without realizing the switch had not taken effect -- and coins mined in the meantime land only in wallet.dat, not the SQLite wallet they think they are now on. The post-conversion screen now offers "Restart Now", which relaunches Bitflash with the same command line and datadir (a short delay lets this instance release the network port and block-index files first) and asks this one to exit, so mining resumes on the SQLite wallet. "Later" keeps the old behaviour. A dimmed line warns that mining before the restart still credits the old wallet. wallet.dat is still never touched. The conversion action itself is unchanged (wallet-convert self-test still green); this is GUI wiring only. --- src/gui.cpp | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 3 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 89a7ab6..81dd8e0 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -11,6 +11,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include #include #include @@ -1400,6 +1403,67 @@ static void DrawAboutDialog() static bool g_showBackendWizard = false; static std::string g_wizardStatus; static bool g_wizardConverted = false; +static int g_restartArgc = 0; +static char** g_restartArgv = NULL; + +// Relaunch Bitflash with the same arguments and datadir, then ask this instance +// to exit. A short delay lets the old process release the network port and the +// block-index files before the new one opens them, so mining resumes cleanly on +// the SQLite wallet instead of both instances fighting over the same files. +static void RestartApplication() +{ +#ifdef _WIN32 + char exePath[MAX_PATH]; + if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0) + return; + std::string strArgs; + bool fHasDataDir = false; + for (int i = 1; i < g_restartArgc; i++) + { + std::string a = g_restartArgv[i] ? g_restartArgv[i] : ""; + if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0) + fHasDataDir = true; + strArgs += " \"" + a + "\""; + } + if (!fHasDataDir) + strArgs += " \"-datadir=" + GetAppDir() + "\""; + + std::string strBat = GetAppDir() + "\\bitflash-restart.bat"; + FILE* pf = fopen(strBat.c_str(), "wb"); + if (pf) + { + // ping is the reliable no-console delay on Windows; timeout needs a tty. + fprintf(pf, + "@echo off\r\n" + "ping 127.0.0.1 -n 3 >nul\r\n" + "start \"\" \"%s\"%s\r\n" + "del \"%%~f0\"\r\n", + exePath, strArgs.c_str()); + fclose(pf); + std::string strCmd = "cmd /c \"" + strBat + "\""; + STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); + PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); + if (CreateProcessA(NULL, (char*)strCmd.c_str(), NULL, NULL, FALSE, + CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) + { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + } +#else + pid_t pid = fork(); + if (pid == 0) + { + sleep(2); + if (g_restartArgv) + execv(g_restartArgv[0], g_restartArgv); + _exit(127); + } +#endif + fShutdown = true; + if (glfwGetCurrentContext()) + glfwSetWindowShouldClose(glfwGetCurrentContext(), true); +} static void DrawBackendWizardDialog() { @@ -1416,11 +1480,19 @@ static void DrawBackendWizardDialog() ImGui::TextWrapped("%s", g_wizardStatus.c_str()); ImGui::Spacing(); ImGui::TextWrapped( - "Restart Bitflash to start using the SQLite wallet. Your Berkeley " - "DB wallet.dat is kept untouched as a fallback."); + "The switch takes effect when Bitflash restarts. Until then it is " + "still running on the old Berkeley DB wallet. Your wallet.dat is " + "kept untouched as a fallback."); ImGui::Spacing(); - if (ImGui::Button("Close", ImVec2(120.0f, 0.0f))) + if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f))) + RestartApplication(); + ImGui::SameLine(); + if (ImGui::Button("Later", ImVec2(120.0f, 0.0f))) g_showBackendWizard = false; + ImGui::Spacing(); + ImGui::TextDisabled( + "If you keep mining before restarting, new coins go to the old " + "wallet until you switch."); } else { @@ -1535,6 +1607,10 @@ int RunGUI(int argc, char* argv[]) g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + // Kept for the wizard's "Restart Now": relaunch with the same command line. + g_restartArgc = argc; + g_restartArgv = argv; + // First run on a Berkeley DB wallet with no recorded choice yet: offer to // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists, // or once a choice has been recorded in the backend marker. -- cgit v1.2.3 From cf155d40ddd260975240e72b5e72cd3decc30291 Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 09:30:40 -0300 Subject: gui/wallet: name backups by backend, and stop saying "wallet.dat" for phrase gaps Two things that read wrong once the wallet is on SQLite: - The Wallet Safety backup dialog defaulted the file name to .dat, but BackupWallet copies wallet.sqlite under the SQLite backend -- so the suggested name put SQLite contents in a wallet-backup-*.dat file. The default now matches the live backend (.sqlite on SQLite, .dat on BDB). - The recovery audit called coins on non-phrase keys "wallet.dat-only" and told the user to "keep wallet.dat backups". Under SQLite those coins live in wallet.sqlite, not wallet.dat. Reworded to the backend-neutral truth: "not phrase-backed" balance, and "keep a file backup of this wallet" -- accurate on either backend. Same wording in the CLI -recoveryaudit and the GUI Wallet Safety panel. Display/naming only; audit numbers and BackupWallet are unchanged. All 13 wallet suites still pass. --- src/gui.cpp | 14 +++++++++----- src/walletcmd.cpp | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 81dd8e0..672f0ff 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -175,7 +175,11 @@ static bool PathIsInsideDataDir(const std::string& strPath) static void SetDefaultBackupPath() { - std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; + // Match the extension to the live backend: BackupWallet copies wallet.sqlite + // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat" + // default name would put SQLite contents in a misleadingly-named file. + const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat"; + std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt; strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); g_backupPath[sizeof(g_backupPath)-1] = '\0'; } @@ -1257,11 +1261,11 @@ static void DrawWalletSafetyDialog() } ImGui::Text("Phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); - ImGui::Text("Wallet.dat-only spendable balance: %s BTF", + ImGui::Text("Not phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); ImGui::Text("Phrase-backed immature mining rewards: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); - ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", + ImGui::Text("Not phrase-backed immature mining rewards: %s BTF", FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); if (!g_recoveryAudit.fDeriveComplete) { @@ -1275,8 +1279,8 @@ static void DrawWalletSafetyDialog() ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped( "Some coins are on keys the phrase does not reproduce. " - "Keep wallet.dat backups until that balance has been moved to a " - "phrase-backed address."); + "Keep a file backup of this wallet until that balance has been " + "moved to a phrase-backed address."); ImGui::PopStyleColor(); } else if (HaveHDSeed() && diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 1f1fd27..f59fc0c 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -429,13 +429,13 @@ int CmdRecoveryAudit() } printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); - printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n", + printf(" not phrase-backed balance: %s BTF (%d transaction(s))\n", FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", FormatMoney(audit.nRecoverableImmatureCredit).c_str(), audit.nRecoverableImmatureTx); - printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n", + printf(" not phrase-backed immature: %s BTF (%d transaction(s))\n", FormatMoney(audit.nLegacyImmatureCredit).c_str(), audit.nLegacyImmatureTx); @@ -457,7 +457,7 @@ int CmdRecoveryAudit() { printf("\n"); printf("Warning: some coins are on keys the phrase does not reproduce.\n"); - printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n"); + printf("Keep a file backup of this wallet until that balance has been moved to a phrase-backed address.\n"); fflush(stdout); return 2; } -- cgit v1.2.3 From d4c49085a10c0f7c3f5a1dcc72184938dca38fea Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 09:48:07 -0300 Subject: gui: harden the restart relaunch (ultra edge cases) Follow-up to the Restart Now button. Addresses the edge cases the review flagged, and drops the fragile batch layer entirely: - Windows no longer writes a .bat and shells through cmd (which re-parsed the arguments and could leave a stray file on failure). It builds the command line once, quoted correctly (only backslashes before a quote are doubled, so ordinary paths are not mangled), and CreateProcess launches the new instance directly. - The delay moved into the new instance: a -restartwait flag makes it wait ~2.5s before it opens the network port and the block index, so the old process is gone first. No shell delay hack. Verified: -restartwait adds the wait (0.16s -> 2.67s). - Linux re-execs /proc/self/exe (absolute) and only falls back to argv[0], which may be relative or a bare PATH name that execv cannot resolve. - This instance now exits only if the relaunch actually started; on failure it keeps running and tells the user to restart manually, so a failed relaunch never leaves them with no node. GUI/process wiring only; wallet code unchanged, suites still green. --- src/gui.cpp | 91 +++++++++++++++++++++++++++++++++++++------------------- src/main_gui.cpp | 8 +++++ 2 files changed, 69 insertions(+), 30 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 672f0ff..296d20c 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1410,63 +1410,94 @@ static bool g_wizardConverted = false; static int g_restartArgc = 0; static char** g_restartArgv = NULL; +#ifdef _WIN32 +// Quote one argument so CommandLineToArgvW parses it back to exactly this +// string: only backslashes that immediately precede a double quote (including +// the closing one) are doubled, so ordinary Windows paths are not mangled. +static std::string QuoteArgWin(const std::string& a) +{ + if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos) + return a; + std::string r = "\""; + size_t bs = 0; + for (size_t i = 0; i < a.size(); i++) + { + if (a[i] == '\\') { bs++; continue; } + if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; } + r.append(bs, '\\'); bs = 0; r += a[i]; + } + r.append(bs * 2, '\\'); + r += "\""; + return r; +} +#endif + // Relaunch Bitflash with the same arguments and datadir, then ask this instance -// to exit. A short delay lets the old process release the network port and the -// block-index files before the new one opens them, so mining resumes cleanly on -// the SQLite wallet instead of both instances fighting over the same files. +// to exit -- but only once the relaunch has actually started, so a failure never +// leaves the user with no node running. The new instance waits (-restartwait) +// before it opens the network port and the block index, so this one is gone +// first and they never fight over the same files. static void RestartApplication() { + bool fLaunched = false; #ifdef _WIN32 char exePath[MAX_PATH]; if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0) return; - std::string strArgs; + std::string strCmd = QuoteArgWin(exePath); bool fHasDataDir = false; for (int i = 1; i < g_restartArgc; i++) { std::string a = g_restartArgv[i] ? g_restartArgv[i] : ""; if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0) fHasDataDir = true; - strArgs += " \"" + a + "\""; + strCmd += " " + QuoteArgWin(a); } if (!fHasDataDir) - strArgs += " \"-datadir=" + GetAppDir() + "\""; - - std::string strBat = GetAppDir() + "\\bitflash-restart.bat"; - FILE* pf = fopen(strBat.c_str(), "wb"); - if (pf) + strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir()); + strCmd += " -restartwait"; + + std::vector cmdBuf(strCmd.begin(), strCmd.end()); + cmdBuf.push_back('\0'); + STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); + PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); + if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - // ping is the reliable no-console delay on Windows; timeout needs a tty. - fprintf(pf, - "@echo off\r\n" - "ping 127.0.0.1 -n 3 >nul\r\n" - "start \"\" \"%s\"%s\r\n" - "del \"%%~f0\"\r\n", - exePath, strArgs.c_str()); - fclose(pf); - std::string strCmd = "cmd /c \"" + strBat + "\""; - STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); - if (CreateProcessA(NULL, (char*)strCmd.c_str(), NULL, NULL, FALSE, - CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) - { - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + fLaunched = true; } #else pid_t pid = fork(); if (pid == 0) { + // Child: wait for the parent to release the port and files, then re-exec. + // /proc/self/exe is the reliable absolute path; argv[0] may be relative + // or a bare name that execv() cannot resolve. sleep(2); + char exePath[4096]; + ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (len > 0) + { + exePath[len] = '\0'; + execv(exePath, g_restartArgv); + } if (g_restartArgv) execv(g_restartArgv[0], g_restartArgv); _exit(127); } + fLaunched = (pid > 0); #endif - fShutdown = true; - if (glfwGetCurrentContext()) - glfwSetWindowShouldClose(glfwGetCurrentContext(), true); + + if (fLaunched) + { + fShutdown = true; + if (glfwGetCurrentContext()) + glfwSetWindowShouldClose(glfwGetCurrentContext(), true); + } + else + g_wizardStatus = "Could not relaunch automatically -- please restart " + "Bitflash yourself to switch to the SQLite wallet."; } static void DrawBackendWizardDialog() diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 6cd8b75..55ca8c6 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -509,6 +509,14 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); +#ifdef _WIN32 + // Set by the GUI "Restart Now" button: give the previous instance a moment + // to release the network port and the block-index files before this one + // opens them, so a relaunch does not race the process it is replacing. + if (arg(argc, argv, "/restartwait") || arg(argc, argv, "-restartwait")) + Sleep(2500); +#endif + // Hidden self-test helper. It must run before LoadWallet(): several // storage-sanity scenarios deliberately make wallet.dat unsafe to load. string strSelfTestMutateWallet = -- cgit v1.2.3 From 2bf8941be913c113e90a62094e6b69848a87ed38 Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 10:00:29 -0300 Subject: gui: show the real version in About, add the website link The About dialog had "v1.1.0" hardcoded and no home for the release version, so it drifted. Add a single BITFLASH_VERSION_STRING in headers_core.h (bumped at release, unrelated to the protocol nVersion fields) and show it in About -- now v1.2.20. Fill the empty line under the version with a clickable https://bitflash.network that opens in the browser (ShellExecute on Windows, xdg-open elsewhere), and grow the dialog to fit it. --- src/gui.cpp | 30 +++++++++++++++++++++++++++--- src/headers_core.h | 5 +++++ 2 files changed, 32 insertions(+), 3 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 296d20c..2a7f350 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -11,7 +11,9 @@ #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#include // ShellExecuteA, to open the website in the browser +#else #include #endif #include @@ -1376,16 +1378,38 @@ static void DrawDiagnosticsDialog() ImGui::End(); } +static void OpenURL(const char* pszUrl) +{ +#ifdef _WIN32 + ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL); +#else + std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &"; + int r = system(cmd.c_str()); + (void)r; +#endif +} + static void DrawAboutDialog() { if (!g_showAbout) return; - ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("About Bitflash", &g_showAbout, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), + "Bitflash BTF v" BITFLASH_VERSION_STRING); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f)); + ImGui::TextUnformatted("https://bitflash.network"); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (ImGui::IsMouseClicked(0)) + OpenURL("https://bitflash.network"); + } ImGui::Spacing(); ImGui::TextWrapped( "CPU-only cryptocurrency. RandomX proof of work. " diff --git a/src/headers_core.h b/src/headers_core.h index 6c0c602..f890c2f 100644 --- a/src/headers_core.h +++ b/src/headers_core.h @@ -1,6 +1,11 @@ // Core node headers -- no GUI framework dependency. // Included by gui.cpp and all non-GUI source files. +// The one place the human-readable release version lives -- bump it at release. +// It is what the About dialog shows. Protocol and transaction versions elsewhere +// (nVersion fields) are unrelated and must not be changed for a release. +#define BITFLASH_VERSION_STRING "1.2.20" + #ifdef _MSC_VER #pragma warning(disable:4786) #endif -- cgit v1.2.3 From b8a976f4870ba5dfc7a6af1ea09c9277b71d686c Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 17:42:24 +0000 Subject: gui: check the two thread spawns the GUI target still leaves unguarded gui.cpp is not in NODE_OBJS, so bitflash-node never sees it -- but it is in GUI_OBJS and `all: bitflash` is the default target, so this is what an operator who runs the windowed build is running. Two spawns there were still unchecked. Enabling pool mode from the options dialog set gPoolServerRunning = true and then called _beginthread without looking at the result, so a refused spawn left the operator with a pool that reports itself running, no RPC server for miners to reach, and nothing in the log. The flag latches, so the branch is never retried either. The restore dialog called std::thread(RestoreThread).detach() bare, which on a refused spawn is an uncaught std::system_error and takes the window down -- the same failure issue #18 was about, in the same tree, one target over. The busy flag stays where it was, set before the spawn and rolled back in the catch. It cannot move after the spawn: RestoreThread clears it itself on the way out, and a restore that finishes quickly would have its `false` overwritten by ours, leaving the dialog greyed out over a search that is not running. Verified by type-checking gui.cpp under the Makefile's exact flags against a stub GLFW/GL header, since this host has neither. That is a syntax and type check, not a link: gui.cpp reaches GLFW through 13 functions and three GL calls, all declared with their real signatures, but the GUI target has not been built here. --- src/gui.cpp | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 8dcb854..9b3802b 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "bip32.h" #include "walletcmd.h" #include "font_roboto.h" @@ -910,8 +911,16 @@ static void DrawOptionsDialog() if (nMineMode == MINE_OPERATOR) { if (!gPoolServerRunning) { - gPoolServerRunning = true; - _beginthread(ThreadRPCServer, 0, NULL); + // Claim the server is up only once it is. The flag used to + // be set first and the result never looked at, so a refused + // spawn left an operator with a pool that reports itself + // running, no RPC server for miners to reach, and nothing + // anywhere saying why. The flag also latches: with it set, + // this branch is never retried. + if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1) + printf("Error: _beginthread(ThreadRPCServer) failed -- pool server not started\n"); + else + gPoolServerRunning = true; } else if (!gPoolRunning) { gPoolRunning = true; } @@ -1124,11 +1133,28 @@ static void DrawRestorePhraseDialog() } g_restoreDerived.store(0); g_restoreRecovered.store(0); + // Set before the spawn, not after: RestoreThread clears this + // flag itself when it finishes, and a short restore can beat + // us back here and have its `false` overwritten, leaving the + // dialog disabled forever. g_restoreRunning.store(true); // On its own thread: a restore walks the whole chain once per // batch and would otherwise freeze the window for as long as it - // takes. - std::thread(RestoreThread, std::string(g_restoreInput)).detach(); + // takes. A refused spawn is one restore that does not happen, + // not a reason to take the window down with an uncaught + // std::system_error -- and the flag has to come back off, or + // the dialog sits greyed out reporting a search nobody is + // running. + try + { + std::thread(RestoreThread, std::string(g_restoreInput)).detach(); + } + catch (const std::system_error& e) + { + g_restoreRunning.store(false); + std::lock_guard lock(g_restoreResultMutex); + g_restoreResult = std::string("Could not start the search: ") + e.what(); + } } } if (fBusy) -- cgit v1.2.3