Skip to content

Commit ed6f396

Browse files
chopperbrianoclaude
andcommitted
Phase 2 dial-back verification + Phase 3 payout pipeline + peerId fix + UI polish (win.33)
Phase 2: dial-back verification (verified working overnight — 413/413 probes passed) New PoolVerifier class runs a background thread every 60 seconds that walks the nodes table, pulls up to 10 least-recently-verified peerIds, and for each one calls the local IPFS node's /api/v0/swarm/connect. Success = the peer is findable and reachable via IPFS. Failure increments verifyFails. Nodes with 3+ consecutive failures are excluded from /nodes.json and from Phase 3 payouts. Self-detection: when the pool server and client run on the same machine, the verifier detects that the peer's multiaddr contains the local IPFS node's own peerId and auto-verifies without attempting swarm/connect (which would fail due to NAT hairpinning on most consumer routers). Schema migration: ALTER TABLE adds lastVerifyOk and verifyFails columns to the nodes table. Existing pool.db files get the migration automatically; fresh databases get all columns in the initial CREATE TABLE. Phase 3: operator-approved payout pipeline (code complete, untested with real DGB) [P] key shows a payout preview: count of verified nodes, pool budget from poolspendperperiod config key, per-node share, and each eligible payout address. Read-only — safe to press at any time. [E] key executes a payout with explicit Y/N confirmation. Guards refuse to execute if poolpayouts=0, poolspendperperiod is missing/zero, no verified nodes, or rpcuser not configured. On confirmation, calls sendtoaddress on the local DigiByte Core wallet via JSON-RPC for each eligible node, logs each txid or error, and records in the payouts_ledger table. Dashboard Payouts row shows real paid-total from the ledger instead of a placeholder. New pool.cfg keys for Phase 3: rpcuser, rpcpassword, rpcport (wallet RPC credentials), poolspendperperiod (DGB to distribute per [E] press). PeerId bug fix IPFS::getPeerId now filters the /id response's address list to only include entries whose trailing /p2p/<id> segment matches the local node's own ID field. Previously, findPublicAddress could return a bootstrap node's multiaddr (104.131.131.82/QmaCpDMG...) as if it were ours. New extractIdField() helper pulls the ID from the /id JSON response. If the filter removes all addresses, falls back to the bare peerId string. Confirmed working: the keepalive log now shows the real peerId /ip4/64.182.71.30/tcp/4001/p2p/12D3KooWQ5wFb1PLNb3i6BET6R... instead of the DigitalOcean bootstrap's Qm-prefixed address. UI polish - DigiAssetCore [H] help rewritten as numbered sub-menus: press H for a 3-line topic index, then 1-6 for detail sections that fit on one screen. No more 40-line dump that scrolls off the log area. - Pool server dashboard now uses the same cell() fixed-column helper as the main exe for aligned two-column status rows. Same COL1_LABEL_W / COL1_VALUE_W / COL2_LABEL_W constants. - Pool server header shows version string matching the main exe's format. - Pool server log area dynamically sizes from terminal height (no blank gap). - Pool server uptime format matches the main exe (N sec / N min / N.N hours). - Pool server [H] handler prints a concise 6-line help block. - Phase 3 keys [P] [E] are dimmed in the key-hints bar since payouts default off. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9cea8fa commit ed6f396

12 files changed

Lines changed: 840 additions & 185 deletions

CMakeLists.txt

Lines changed: 1 addition & 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 32)
43+
SET(WIN_BUILD 33)
4444

4545
# Add source directory
4646
include_directories(src)

pool/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ set(HEADER_FILES
2929
PoolDatabase.h
3030
PoolServer.h
3131
PoolDashboard.h
32+
PoolVerifier.h
3233
)
3334

3435
set(SOURCE_FILES
3536
main.cpp
3637
PoolDatabase.cpp
3738
PoolServer.cpp
3839
PoolDashboard.cpp
40+
PoolVerifier.cpp
3941
# Borrow sqlite3.c and curl_stubs.cpp from the main src/ tree so the
4042
# pool server doesn't need its own copies. sqlite for the pool db,
4143
# curl_stubs for the first-run HTTP snapshot of mctrivia's /permanent.

pool/PoolDashboard.cpp

Lines changed: 284 additions & 56 deletions
Large diffs are not rendered by default.

pool/PoolDashboard.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020

2121
class PoolDatabase;
2222
class PoolServer;
23+
class PoolVerifier;
2324

2425
class PoolDashboard {
2526
public:
26-
PoolDashboard(PoolDatabase& db, PoolServer& server);
27+
// configPath = path to pool.cfg, re-read on each [E] press so
28+
// the operator can adjust poolspendperperiod without restarting.
29+
PoolDashboard(PoolDatabase& db, PoolServer& server, PoolVerifier& verifier,
30+
const std::string& configPath = "pool.cfg");
2731
~PoolDashboard();
2832

2933
// Enable VT100 escape sequences on the Windows console. Returns true if
@@ -41,14 +45,21 @@ class PoolDashboard {
4145
private:
4246
PoolDatabase& _db;
4347
PoolServer& _server;
48+
PoolVerifier& _verifier;
49+
std::string _configPath;
4450
std::atomic<bool> _running{false};
51+
std::atomic<bool> _awaitingPayoutConfirm{false};
4552
std::atomic<bool> _quit{false};
4653
std::thread _thread;
4754
std::chrono::system_clock::time_point _startTime;
4855

4956
std::mutex _logMutex;
5057
std::deque<std::string> _logLines;
51-
static constexpr size_t MAX_LOG_LINES = 20;
58+
static constexpr size_t MAX_LOG_LINES = 200;
59+
60+
int _width = 120;
61+
int _height = 40;
62+
void updateConsoleSize();
5263

5364
void refreshLoop();
5465
void render();

pool/PoolDatabase.cpp

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ void PoolDatabase::buildSchema() {
8888
");"
8989
);
9090

91+
// Schema migration: win.33 adds dial-back verification columns. These
92+
// ALTER TABLEs fail silently if the columns already exist (sqlite's
93+
// ALTER doesn't have IF NOT EXISTS; we catch the error and move on).
94+
auto tryAddColumn = [this](const char* sql) {
95+
char* errMsg = nullptr;
96+
int rc = sqlite3_exec(_db, sql, nullptr, nullptr, &errMsg);
97+
if (rc != SQLITE_OK && errMsg) {
98+
// Ignore "duplicate column name" - that just means the migration
99+
// already ran on a previous launch against this pool.db.
100+
sqlite3_free(errMsg);
101+
}
102+
};
103+
tryAddColumn("ALTER TABLE nodes ADD COLUMN lastVerifyOk INTEGER NOT NULL DEFAULT 0;");
104+
tryAddColumn("ALTER TABLE nodes ADD COLUMN verifyFails INTEGER NOT NULL DEFAULT 0;");
105+
91106
// permanent_assets rows are imported from the first-run snapshot of
92107
// mctrivia's /permanent/<page>.json or added later by the operator.
93108
// Primary key includes cid because a single assetId can reference
@@ -169,6 +184,150 @@ void PoolDatabase::upsertNode(const std::string& peerId,
169184
}
170185
}
171186

187+
void PoolDatabase::recordVerifySuccess(const std::string& peerId) {
188+
std::lock_guard<std::mutex> lk(_mutex);
189+
int64_t now = nowUnix();
190+
const char* sql =
191+
"UPDATE nodes SET lastVerifyOk = ?, verifyFails = 0 WHERE peerId = ?;";
192+
sqlite3_stmt* stmt = nullptr;
193+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return;
194+
sqlite3_bind_int64(stmt, 1, now);
195+
sqlite3_bind_text(stmt, 2, peerId.c_str(), -1, SQLITE_TRANSIENT);
196+
sqlite3_step(stmt);
197+
sqlite3_finalize(stmt);
198+
}
199+
200+
void PoolDatabase::recordVerifyFailure(const std::string& peerId) {
201+
std::lock_guard<std::mutex> lk(_mutex);
202+
const char* sql =
203+
"UPDATE nodes SET verifyFails = verifyFails + 1 WHERE peerId = ?;";
204+
sqlite3_stmt* stmt = nullptr;
205+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return;
206+
sqlite3_bind_text(stmt, 1, peerId.c_str(), -1, SQLITE_TRANSIENT);
207+
sqlite3_step(stmt);
208+
sqlite3_finalize(stmt);
209+
}
210+
211+
std::vector<std::string> PoolDatabase::getPeerIdsForVerification(unsigned int limit) {
212+
std::lock_guard<std::mutex> lk(_mutex);
213+
// Least-recently-verified first. Also favors nodes we've never verified
214+
// (lastVerifyOk = 0 puts them at the top of the order). Still seen in
215+
// the last week, so we don't waste time probing ghosts.
216+
int64_t cutoff = nowUnix() - 7 * 24 * 60 * 60;
217+
std::vector<std::string> out;
218+
const char* sql =
219+
"SELECT peerId FROM nodes WHERE lastSeen >= ? "
220+
"ORDER BY lastVerifyOk ASC LIMIT ?;";
221+
sqlite3_stmt* stmt = nullptr;
222+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
223+
sqlite3_bind_int64(stmt, 1, cutoff);
224+
sqlite3_bind_int(stmt, 2, (int) limit);
225+
while (sqlite3_step(stmt) == SQLITE_ROW) {
226+
const unsigned char* p = sqlite3_column_text(stmt, 0);
227+
if (p) out.emplace_back(reinterpret_cast<const char*>(p));
228+
}
229+
sqlite3_finalize(stmt);
230+
}
231+
return out;
232+
}
233+
234+
unsigned int PoolDatabase::countVerifiedSince(int64_t unixSeconds) {
235+
std::lock_guard<std::mutex> lk(_mutex);
236+
const char* sql = "SELECT COUNT(*) FROM nodes WHERE lastVerifyOk >= ?;";
237+
sqlite3_stmt* stmt = nullptr;
238+
unsigned int count = 0;
239+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
240+
sqlite3_bind_int64(stmt, 1, unixSeconds);
241+
if (sqlite3_step(stmt) == SQLITE_ROW) count = (unsigned int) sqlite3_column_int(stmt, 0);
242+
sqlite3_finalize(stmt);
243+
}
244+
return count;
245+
}
246+
247+
unsigned int PoolDatabase::countFailedOut() {
248+
std::lock_guard<std::mutex> lk(_mutex);
249+
const char* sql = "SELECT COUNT(*) FROM nodes WHERE verifyFails >= 3;";
250+
sqlite3_stmt* stmt = nullptr;
251+
unsigned int count = 0;
252+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
253+
if (sqlite3_step(stmt) == SQLITE_ROW) count = (unsigned int) sqlite3_column_int(stmt, 0);
254+
sqlite3_finalize(stmt);
255+
}
256+
return count;
257+
}
258+
259+
std::vector<PoolDatabase::PayoutTarget> PoolDatabase::getVerifiedPayoutTargets() {
260+
std::lock_guard<std::mutex> lk(_mutex);
261+
int64_t now = nowUnix();
262+
int64_t seenCutoff = now - 7 * 24 * 60 * 60; // seen in last 7 days
263+
int64_t verifyCutoff = now - 24 * 60 * 60; // verified in last 24h
264+
265+
std::vector<PayoutTarget> out;
266+
const char* sql =
267+
"SELECT peerId, payoutAddress FROM nodes "
268+
"WHERE lastSeen >= ? AND lastVerifyOk >= ? AND verifyFails < 3 "
269+
"ORDER BY peerId;";
270+
sqlite3_stmt* stmt = nullptr;
271+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
272+
sqlite3_bind_int64(stmt, 1, seenCutoff);
273+
sqlite3_bind_int64(stmt, 2, verifyCutoff);
274+
while (sqlite3_step(stmt) == SQLITE_ROW) {
275+
PayoutTarget t;
276+
const unsigned char* p = sqlite3_column_text(stmt, 0);
277+
const unsigned char* a = sqlite3_column_text(stmt, 1);
278+
if (p) t.peerId = (const char*) p;
279+
if (a) t.payoutAddress = (const char*) a;
280+
if (!t.payoutAddress.empty()) out.push_back(t);
281+
}
282+
sqlite3_finalize(stmt);
283+
}
284+
return out;
285+
}
286+
287+
void PoolDatabase::recordPayout(const std::string& payoutAddress, int64_t amountDgbSat,
288+
const std::string& txid) {
289+
std::lock_guard<std::mutex> lk(_mutex);
290+
int64_t now = nowUnix();
291+
const char* sql =
292+
"INSERT INTO payouts_ledger (payoutAddress, amountDgbSat, owedAt, paidTxid, paidAt) "
293+
"VALUES (?, ?, ?, ?, ?);";
294+
sqlite3_stmt* stmt = nullptr;
295+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return;
296+
sqlite3_bind_text(stmt, 1, payoutAddress.c_str(), -1, SQLITE_TRANSIENT);
297+
sqlite3_bind_int64(stmt, 2, amountDgbSat);
298+
sqlite3_bind_int64(stmt, 3, now);
299+
sqlite3_bind_text(stmt, 4, txid.c_str(), -1, SQLITE_TRANSIENT);
300+
sqlite3_bind_int64(stmt, 5, now);
301+
sqlite3_step(stmt);
302+
sqlite3_finalize(stmt);
303+
}
304+
305+
double PoolDatabase::getPaidTotalDgb() {
306+
std::lock_guard<std::mutex> lk(_mutex);
307+
const char* sql = "SELECT COALESCE(SUM(amountDgbSat), 0) FROM payouts_ledger WHERE paidTxid IS NOT NULL;";
308+
sqlite3_stmt* stmt = nullptr;
309+
double total = 0.0;
310+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
311+
if (sqlite3_step(stmt) == SQLITE_ROW) {
312+
total = sqlite3_column_int64(stmt, 0) / 100000000.0;
313+
}
314+
sqlite3_finalize(stmt);
315+
}
316+
return total;
317+
}
318+
319+
unsigned int PoolDatabase::getPaidCount() {
320+
std::lock_guard<std::mutex> lk(_mutex);
321+
const char* sql = "SELECT COUNT(*) FROM payouts_ledger WHERE paidTxid IS NOT NULL;";
322+
sqlite3_stmt* stmt = nullptr;
323+
unsigned int count = 0;
324+
if (sqlite3_prepare_v2(_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
325+
if (sqlite3_step(stmt) == SQLITE_ROW) count = (unsigned int) sqlite3_column_int(stmt, 0);
326+
sqlite3_finalize(stmt);
327+
}
328+
return count;
329+
}
330+
172331
void PoolDatabase::insertPermanentAsset(const std::string& assetId,
173332
const std::string& txHash,
174333
const std::string& cid,
@@ -301,11 +460,16 @@ std::string PoolDatabase::buildPermanentPageJson(unsigned int page) {
301460

302461
std::string PoolDatabase::buildNodesJson() {
303462
std::lock_guard<std::mutex> lk(_mutex);
304-
// 7-day window: any node we've seen in the last week is "online enough"
305-
// to list on /nodes.json.
463+
// 7-day window, AND exclude nodes that have failed dial-back
464+
// verification 3+ times in a row. Failed-out nodes are ghosts — they
465+
// keep sending keepalive pings but the pool operator can't actually
466+
// reach them via IPFS, so other clients shouldn't see them either.
306467
int64_t cutoff = nowUnix() - 7 * 24 * 60 * 60;
307468

308-
const char* sql = "SELECT peerId FROM nodes WHERE lastSeen >= ? ORDER BY lastSeen DESC;";
469+
const char* sql =
470+
"SELECT peerId FROM nodes "
471+
"WHERE lastSeen >= ? AND verifyFails < 3 "
472+
"ORDER BY lastSeen DESC;";
309473
sqlite3_stmt* stmt = nullptr;
310474
std::ostringstream json;
311475
json << "[";

pool/PoolDatabase.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ class PoolDatabase {
2929
void upsertNode(const std::string& peerId,
3030
const std::string& payoutAddress);
3131

32+
// Dial-back verification state. The PoolVerifier thread calls these
33+
// after each probe attempt.
34+
// recordVerifySuccess: bumps lastVerifyOk, resets verifyFails to 0
35+
// recordVerifyFailure: increments verifyFails (lastVerifyOk unchanged)
36+
void recordVerifySuccess(const std::string& peerId);
37+
void recordVerifyFailure(const std::string& peerId);
38+
39+
// Returns up to `limit` peerIds the verifier should probe next,
40+
// ordered by lastVerifyOk ascending (least-recently-verified first).
41+
// Used by PoolVerifier to pick work each iteration.
42+
std::vector<std::string> getPeerIdsForVerification(unsigned int limit);
43+
44+
// Counts for the dashboard Verified: row.
45+
unsigned int countVerifiedSince(int64_t unixSeconds);
46+
unsigned int countFailedOut(); // nodes with verifyFails >= 3
47+
3248
// Permanent assets (one row per (assetId, txHash, cid) tuple).
3349
// Used by the first-run snapshot and, later, operator-added entries.
3450
void insertPermanentAsset(const std::string& assetId,
@@ -62,6 +78,23 @@ class PoolDatabase {
6278
unsigned int countPermanentAssets();
6379
unsigned int countPermanentPages();
6480

81+
// Phase 3: payout support.
82+
// Returns (peerId, payoutAddress) pairs for nodes eligible for payout:
83+
// lastVerifyOk within last 24h AND verifyFails < 3 AND lastSeen within 7 days.
84+
struct PayoutTarget {
85+
std::string peerId;
86+
std::string payoutAddress;
87+
};
88+
std::vector<PayoutTarget> getVerifiedPayoutTargets();
89+
90+
// Record a completed payout in the ledger.
91+
void recordPayout(const std::string& payoutAddress, int64_t amountDgbSat,
92+
const std::string& txid);
93+
94+
// Ledger totals for dashboard display.
95+
double getPaidTotalDgb(); // sum of all paid-out DGB
96+
unsigned int getPaidCount(); // number of payout transactions
97+
6598
// Pool-local config key/value store (separate from the operator's
6699
// editable pool.cfg; this is runtime state like "last snapshot time").
67100
void setConfig(const std::string& key, const std::string& value);

0 commit comments

Comments
 (0)