Skip to content

Commit 9cea8fa

Browse files
chopperbrianoclaude
andcommitted
DigiAssetPoolServer.exe Phase 1 + psp1server client key + honest Payment row (win.32)
The first real tool for getting off mctrivia's dead pool server. === New: DigiAssetPoolServer.exe === New pool/ subdirectory builds an optional DigiAssetPoolServer.exe as a companion to the main exe. Same cmake, same Boost, same SQLite amalgamation already used by the main exe's chain.db. Ships in every release alongside DigiAssetCore.exe; completely optional — only pool operators need to run it. Implements mctrivia's wire protocol so existing win.31+ clients (and the legacy NodeJS digiasset_node client) can register payout addresses and fetch the permanent asset list from a pool the operator controls rather than ipfs.digiassetx.com: GET /permanent/<page>.json - canonical asset/CID list, built from pool.db POST /list/<floor>.json - registration endpoint; returns {"payoutsEnabled": bool, "phase": N, "changes": {}} POST /keepalive - activity ping; returns the "unsubscribe failed will time out anyways" sentinel the protocol expects GET /nodes.json - registered peer list GET /map.json - one blank-geo entry per active node so the existing node-count heuristic works GET /bad.json - empty {"assets":[],"cids":[]} placeholder SQLite pool.db with tables for nodes, permanent_assets, permanent_pages, payouts_ledger (Phase 3 shell), and pool_config. On first run, the pool server bootstraps its permanent list by fetching mctrivia's current /permanent/0..23.json pages and importing them. After the snapshot, the operator owns the data. Takes ~10-30 seconds depending on network; shows live progress in the dashboard log. Own minimal TUI dashboard (VT100, same style as ConsoleDashboard) with: Listening: port + request count Registered: total nodes / active (last hour) nodes Permanent: asset count / page count Payouts: ENABLED (green) or disabled (yellow) Time / Uptime Log area + key hints [Q] [N] [A] [P] [E] [H] === New client config key: psp1server === Adds psp1server config key to mctrivia.cpp. Default "https://ipfs.digiassetx.com" preserves upstream behavior; users pointing at a new pool just add a line to config.cfg: psp1server=http://127.0.0.1:14028 Replaces every previously-hardcoded MCTRIVIA_BASE in mctrivia.cpp — the permanent-list fetcher, the /list probe, the keepalive, the bad list, and getURL(). Trailing slash stripped on load so url concatenation is clean regardless of how the user wrote the value. === Honest "Payment: active" state === Previously the client printed green "Payment: active" the moment ANY pool server accepted a /list request. With this commit the client now parses the new payoutsEnabled bool from the /list response body and the dashboard distinguishes four states: green "active" - pool ok AND payoutsEnabled=true yellow "registered (no payouts yet)" - pool ok, payoutsEnabled=false (Phase 1 local pool OR legacy server that doesn't send the field) red "unavailable" - /list probe fails (mctrivia's broken-since-2024 server) dim "checking..." - not probed yet On the pool server side, new config key poolpayouts (default 0). When 0, the server sends {"payoutsEnabled": false} and the dashboard Payouts row shows yellow "disabled". When 1, sends {"payoutsEnabled": true}, green "ENABLED", AND pre-dashboard stdout prints a big WARNING block about Phase 3 automated distribution not being built yet. Operator has to go out of their way to set it to 1. End-to-end smoke test verified both branches: clients correctly reflect the pool's declared state, and switching poolpayouts between 0 and 1 on the pool server flips every connected client's dashboard appropriately. === Bug fix: keepalive log string === The "Reported online to ipfs.digiassetx.com (server id: ...)" log line was hardcoded, so even when talking to a different pool server (including localhost) the log lied about where the keepalive went. Now uses _baseUrl so the log line matches the actual target. === Expanded [H] help === Dashboard [H] info key now explains all four Payment states in plain English and documents the psp1server config key so operators know how to point at a different pool server. === Known issues === - Phase 3 (operator-approved automated payout distribution via local DigiByte Core RPC) is not yet built. Pool server shows a WARNING if poolpayouts=1 is set before Phase 3 ships. Operators who need to pay right now do it manually from digibyte-qt with the payouts_ledger table as a reference. - Phase 2 (dial-back verification of registered peers) is not yet built. Any client that successfully POSTs /list gets marked registered; no attempt to verify they're actually serving content. - Pre-existing peerId bug: findPublicAddress sometimes returns a bootstrap node's address (104.131.131.82) instead of the local node's address. Unrelated to this commit; flagged for a future win. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b5812f8 commit 9cea8fa

12 files changed

Lines changed: 1783 additions & 18 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ SET(PATCH_VERSION 0)
4040
SET(SO_VERSION 0)
4141

4242
# Windows port build number (increment for each Windows-specific release)
43-
SET(WIN_BUILD 31)
43+
SET(WIN_BUILD 32)
4444

4545
# Add source directory
4646
include_directories(src)
@@ -52,6 +52,12 @@ if(BUILD_CLI)
5252
ADD_SUBDIRECTORY(cli)
5353
endif()
5454

55+
#create optional pool server (Windows-only for now; depends on real Boost
56+
# from packages/ and winhttp, so it only makes sense to build on Windows).
57+
if(WIN32 AND MSVC)
58+
ADD_SUBDIRECTORY(pool)
59+
endif()
60+
5561
#create web
5662
if(BUILD_WEB)
5763
include_directories(web)

pool/CMakeLists.txt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
cmake_minimum_required(VERSION 3.21)
2+
project(DigiAssetPoolServer)
3+
4+
# Pool server - optional companion to DigiAssetCore.exe.
5+
#
6+
# Implements the wire protocol mctrivia's original pool server used, so both
7+
# the win.31+ C++ client and the legacy NodeJS digiasset_node client can
8+
# register payout addresses and fetch the permanent asset list from a pool
9+
# this operator controls.
10+
#
11+
# Same C++ toolchain, same Boost, same SQLite as the main DigiAssetCore
12+
# project. Ships as a standalone DigiAssetPoolServer.exe alongside the main
13+
# exe in each release. Optional to run — only pool operators need it.
14+
15+
set(CMAKE_CXX_STANDARD 17)
16+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
17+
18+
# Real Boost headers first, ahead of anything else. src/boost/asio.hpp in the
19+
# repo is a historical no-op stub we do NOT want to accidentally pick up.
20+
# (See memory/project_boost_stub_trap.md for why.)
21+
if(WIN32 AND MSVC)
22+
include_directories(BEFORE "${CMAKE_SOURCE_DIR}/packages/boost.1.82.0/lib/native/include")
23+
endif()
24+
25+
include_directories("${CMAKE_SOURCE_DIR}/jsoncpp/install/include")
26+
include_directories("${CMAKE_SOURCE_DIR}/src")
27+
28+
set(HEADER_FILES
29+
PoolDatabase.h
30+
PoolServer.h
31+
PoolDashboard.h
32+
)
33+
34+
set(SOURCE_FILES
35+
main.cpp
36+
PoolDatabase.cpp
37+
PoolServer.cpp
38+
PoolDashboard.cpp
39+
# Borrow sqlite3.c and curl_stubs.cpp from the main src/ tree so the
40+
# pool server doesn't need its own copies. sqlite for the pool db,
41+
# curl_stubs for the first-run HTTP snapshot of mctrivia's /permanent.
42+
../src/sqlite3.c
43+
../src/curl_stubs.cpp
44+
../src/CurlHandler.cpp
45+
)
46+
47+
add_executable(DigiAssetPoolServer ${SOURCE_FILES} ${HEADER_FILES})
48+
49+
# Link jsoncpp (same static lib the main exe uses)
50+
target_link_libraries(DigiAssetPoolServer PRIVATE
51+
"${CMAKE_SOURCE_DIR}/jsoncpp/install/lib/jsoncpp_static.lib")
52+
53+
# Windows-specific: winhttp for curl_stubs, winsock for raw TCP
54+
if(WIN32 AND MSVC)
55+
target_link_libraries(DigiAssetPoolServer PRIVATE winhttp)
56+
target_link_libraries(DigiAssetPoolServer PRIVATE ws2_32)
57+
# Boost Beast uses some threading primitives
58+
target_link_libraries(DigiAssetPoolServer PRIVATE bcrypt)
59+
endif()
60+
61+
install(TARGETS DigiAssetPoolServer DESTINATION bin)

pool/PoolDashboard.cpp

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#include "PoolDashboard.h"
2+
#include "PoolDatabase.h"
3+
#include "PoolServer.h"
4+
#include <iostream>
5+
#include <sstream>
6+
#include <iomanip>
7+
8+
#ifdef _WIN32
9+
#include <windows.h>
10+
#include <conio.h>
11+
#endif
12+
13+
// --- VT100 escape helpers --- (same subset ConsoleDashboard uses in main exe)
14+
#define ESC "\033["
15+
#define CURSOR_HOME ESC "H"
16+
#define ERASE_SCREEN ESC "2J"
17+
#define ERASE_LINE ESC "2K"
18+
#define HIDE_CURSOR ESC "?25l"
19+
#define SHOW_CURSOR ESC "?25h"
20+
#define BOLD ESC "1m"
21+
#define DIM ESC "2m"
22+
#define RESET ESC "0m"
23+
#define FG_GREEN ESC "32m"
24+
#define FG_YELLOW ESC "33m"
25+
#define FG_RED ESC "31m"
26+
#define FG_CYAN ESC "36m"
27+
#define FG_BRIGHT_WHITE ESC "97m"
28+
29+
namespace {
30+
std::string formatDuration(int64_t seconds) {
31+
int d = (int) (seconds / 86400);
32+
seconds %= 86400;
33+
int h = (int) (seconds / 3600);
34+
seconds %= 3600;
35+
int m = (int) (seconds / 60);
36+
int s = (int) (seconds % 60);
37+
std::ostringstream out;
38+
if (d > 0) out << d << "d ";
39+
if (d > 0 || h > 0) {
40+
out << std::setfill('0') << std::setw(2) << h << ":"
41+
<< std::setfill('0') << std::setw(2) << m << ":"
42+
<< std::setfill('0') << std::setw(2) << s;
43+
} else {
44+
out << m << " min " << s << " sec";
45+
}
46+
return out.str();
47+
}
48+
49+
std::string formatNumber(uint64_t n) {
50+
std::string s = std::to_string(n);
51+
std::string out;
52+
int count = 0;
53+
for (auto it = s.rbegin(); it != s.rend(); ++it) {
54+
if (count > 0 && count % 3 == 0) out += ',';
55+
out += *it;
56+
count++;
57+
}
58+
std::reverse(out.begin(), out.end());
59+
return out;
60+
}
61+
}
62+
63+
PoolDashboard::PoolDashboard(PoolDatabase& db, PoolServer& server)
64+
: _db(db), _server(server), _startTime(std::chrono::system_clock::now()) {
65+
}
66+
67+
PoolDashboard::~PoolDashboard() {
68+
stop();
69+
}
70+
71+
bool PoolDashboard::enableVT100() {
72+
#ifdef _WIN32
73+
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
74+
if (hOut == INVALID_HANDLE_VALUE) return false;
75+
DWORD mode = 0;
76+
if (!GetConsoleMode(hOut, &mode)) return false;
77+
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
78+
return SetConsoleMode(hOut, mode) != 0;
79+
#else
80+
return true;
81+
#endif
82+
}
83+
84+
void PoolDashboard::start() {
85+
if (_running.exchange(true)) return;
86+
std::cout << ESC "2J" << CURSOR_HOME << HIDE_CURSOR << std::flush;
87+
render();
88+
_thread = std::thread([this]() { this->refreshLoop(); });
89+
}
90+
91+
void PoolDashboard::stop() {
92+
if (!_running.exchange(false)) return;
93+
if (_thread.joinable()) _thread.join();
94+
std::cout << SHOW_CURSOR << std::flush;
95+
}
96+
97+
void PoolDashboard::addLog(const std::string& line) {
98+
std::lock_guard<std::mutex> lk(_logMutex);
99+
_logLines.push_back(line);
100+
while (_logLines.size() > MAX_LOG_LINES) _logLines.pop_front();
101+
}
102+
103+
void PoolDashboard::refreshLoop() {
104+
while (_running.load()) {
105+
processInput();
106+
render();
107+
std::this_thread::sleep_for(std::chrono::milliseconds(500));
108+
}
109+
}
110+
111+
void PoolDashboard::processInput() {
112+
#ifdef _WIN32
113+
while (_kbhit()) {
114+
int ch = _getch();
115+
if (ch == 'q' || ch == 'Q' || ch == 3 /* Ctrl+C */) {
116+
_quit.store(true);
117+
} else if (ch == 'p' || ch == 'P') {
118+
addLog("Pending payouts: 0.0000 DGB (Phase 3 not yet implemented)");
119+
} else if (ch == 'e' || ch == 'E') {
120+
addLog("Execute payout: Phase 3 not yet implemented");
121+
} else if (ch == 'n' || ch == 'N') {
122+
addLog("Registered nodes: " + std::to_string(_db.countTotalNodes()));
123+
} else if (ch == 'a' || ch == 'A') {
124+
addLog("Permanent assets: " + std::to_string(_db.countPermanentAssets()) +
125+
" across " + std::to_string(_db.countPermanentPages()) + " pages");
126+
} else if (ch == 'h' || ch == 'H' || ch == '?') {
127+
addLog("Pool Server - keys: Q=Quit N=Node count A=Asset count P=Pending payouts E=Execute payout");
128+
}
129+
}
130+
#endif
131+
}
132+
133+
void PoolDashboard::render() {
134+
std::ostringstream out;
135+
out << HIDE_CURSOR << CURSOR_HOME;
136+
137+
const int w = 90;
138+
auto separator = [&]() { out << ERASE_LINE << std::string(w, '-') << "\n"; };
139+
140+
// Header
141+
std::string title = "DigiAsset Pool Server (experimental) - Phase 1";
142+
int pad = (w - (int) title.size()) / 2;
143+
out << BOLD << FG_BRIGHT_WHITE << ERASE_LINE
144+
<< std::string(pad > 0 ? pad : 0, ' ') << title << RESET << "\n";
145+
separator();
146+
147+
// Status rows
148+
auto now = std::chrono::system_clock::now();
149+
int64_t uptime = std::chrono::duration_cast<std::chrono::seconds>(now - _startTime).count();
150+
int64_t oneHourAgo = std::chrono::duration_cast<std::chrono::seconds>(
151+
now.time_since_epoch()).count() - 3600;
152+
153+
unsigned int totalNodes = _db.countTotalNodes();
154+
unsigned int activeNodes = _db.countNodesSeenSince(oneHourAgo);
155+
unsigned int permAssets = _db.countPermanentAssets();
156+
unsigned int permPages = _db.countPermanentPages();
157+
uint64_t requests = _server.getRequestCount();
158+
159+
out << ERASE_LINE << " " << "Listening: " << FG_GREEN << "Port " << _server.getPort() << RESET
160+
<< " " << "Requests: " << FG_BRIGHT_WHITE << formatNumber(requests) << RESET << "\n";
161+
162+
out << ERASE_LINE << " " << "Registered: " << FG_BRIGHT_WHITE << totalNodes << " nodes" << RESET
163+
<< " " << "Active (1h): " << FG_BRIGHT_WHITE << activeNodes << " nodes" << RESET << "\n";
164+
165+
out << ERASE_LINE << " " << "Permanent: " << FG_BRIGHT_WHITE << formatNumber(permAssets)
166+
<< " assets" << RESET << " / " << FG_BRIGHT_WHITE << permPages << " pages" << RESET << "\n";
167+
168+
// Payout row. Shows the poolpayouts config state prominently so the
169+
// operator always knows what clients are seeing. If poolpayouts is
170+
// still disabled (Phase 1 default), say so in yellow. When Phase 3
171+
// flips to enabled, switch to green.
172+
{
173+
bool payoutsEnabled = _server.getPayoutsEnabled();
174+
out << ERASE_LINE << " " << "Payouts: ";
175+
if (payoutsEnabled) {
176+
out << FG_GREEN << "ENABLED" << RESET << DIM
177+
<< " (Pending: 0.0000 DGB / Paid: 0.0000 DGB)" << RESET;
178+
} else {
179+
out << FG_YELLOW << "disabled" << RESET << DIM
180+
<< " (clients see 'registered (no payouts yet)')" << RESET;
181+
}
182+
out << "\n";
183+
}
184+
185+
// Time row
186+
auto t = std::chrono::system_clock::to_time_t(now);
187+
std::tm tmBuf{};
188+
#ifdef _WIN32
189+
localtime_s(&tmBuf, &t);
190+
#else
191+
tmBuf = *std::localtime(&t);
192+
#endif
193+
char timeStr[16];
194+
std::strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &tmBuf);
195+
196+
out << ERASE_LINE << " " << "Time: " << FG_BRIGHT_WHITE << timeStr << RESET
197+
<< " " << "Uptime: " << FG_BRIGHT_WHITE << formatDuration(uptime) << RESET << "\n";
198+
199+
separator();
200+
201+
// Log area
202+
out << BOLD << ERASE_LINE << " Log:" << RESET << "\n";
203+
{
204+
std::lock_guard<std::mutex> lk(_logMutex);
205+
for (const auto& line: _logLines) {
206+
out << ERASE_LINE << " " << line << "\n";
207+
}
208+
// Pad to MAX_LOG_LINES so the key-hint row sits in a stable position.
209+
for (size_t i = _logLines.size(); i < MAX_LOG_LINES; i++) {
210+
out << ERASE_LINE << "\n";
211+
}
212+
}
213+
214+
// Key hints row
215+
out << ERASE_LINE << DIM
216+
<< " [Q] Quit [N] Nodes [A] Assets [P] Pending Payouts [E] Execute Payout [H] Help"
217+
<< RESET;
218+
219+
// Clear to end of screen to scrub any stale output from longer renders.
220+
out << ESC "J";
221+
222+
std::cout << out.str() << std::flush;
223+
}

pool/PoolDashboard.h

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// PoolDashboard - minimal VT100 TUI for the pool server exe.
3+
//
4+
// Deliberately separate from the main DigiAssetCore ConsoleDashboard so the
5+
// pool server is a standalone exe with no link-time dependency on the main
6+
// lib. Reimplements the minimum set of helpers we need (VT100 init, cursor
7+
// home, ERASE_LINE, FG colors, key polling, a log buffer).
8+
//
9+
10+
#ifndef DIGIASSET_POOL_DASHBOARD_H
11+
#define DIGIASSET_POOL_DASHBOARD_H
12+
13+
#include <atomic>
14+
#include <chrono>
15+
#include <deque>
16+
#include <functional>
17+
#include <mutex>
18+
#include <string>
19+
#include <thread>
20+
21+
class PoolDatabase;
22+
class PoolServer;
23+
24+
class PoolDashboard {
25+
public:
26+
PoolDashboard(PoolDatabase& db, PoolServer& server);
27+
~PoolDashboard();
28+
29+
// Enable VT100 escape sequences on the Windows console. Returns true if
30+
// the terminal is capable — if false, the caller should fall back to
31+
// printing plain lines.
32+
static bool enableVT100();
33+
34+
void start();
35+
void stop();
36+
37+
void addLog(const std::string& line);
38+
39+
bool quitRequested() const { return _quit.load(); }
40+
41+
private:
42+
PoolDatabase& _db;
43+
PoolServer& _server;
44+
std::atomic<bool> _running{false};
45+
std::atomic<bool> _quit{false};
46+
std::thread _thread;
47+
std::chrono::system_clock::time_point _startTime;
48+
49+
std::mutex _logMutex;
50+
std::deque<std::string> _logLines;
51+
static constexpr size_t MAX_LOG_LINES = 20;
52+
53+
void refreshLoop();
54+
void render();
55+
void processInput();
56+
};
57+
58+
#endif // DIGIASSET_POOL_DASHBOARD_H

0 commit comments

Comments
 (0)