diff --git a/CMakeLists.txt b/CMakeLists.txt index f69bb44..11c3189 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(POLICY CMP0167) endif() project(Triangles - VERSION 5.8.6 + VERSION 5.9.0 DESCRIPTION "Cryptographic Triangles Wallet" LANGUAGES C CXX ) diff --git a/src/clientversion.h b/src/clientversion.h index fbba817..a4e815e 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -7,8 +7,8 @@ // These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 5 -#define CLIENT_VERSION_MINOR 8 -#define CLIENT_VERSION_REVISION 6 +#define CLIENT_VERSION_MINOR 9 +#define CLIENT_VERSION_REVISION 0 #define CLIENT_VERSION_BUILD 0 // Converts the parameter X to a string after macro replacement on X has been performed. diff --git a/src/main.cpp b/src/main.cpp index 2c566cf..bed13ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -139,7 +139,7 @@ static const unsigned int HEADER_DOWNLOAD_WINDOW = 512; // Increased from 128 f static const unsigned int HEADER_DOWNLOAD_PER_PEER = 32; // Reduced from 64 for Tor circuit stability static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 60 * 1000000; // 60s for Tor latency (was 30s) static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 5 * 1000000; // 5s redundant request (reduced for Tor) -static const int64_t HEADER_SYNC_TTL_MICROS = 5 * 60 * 1000000; // 5-minute TTL for cache entries +static const int64_t HEADER_SYNC_TTL_MICROS = 15 * 60 * 1000000; // 15-minute TTL for cache entries (extended for Tor latency) static void ThreadPostIbdWork(void* parg) { @@ -4461,8 +4461,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->PushAddress(addr); } - // Get recent addresses - if (pfrom->fOneShot || pfrom->nVersion >= CADDR_TIME_VERSION || addrman.size() < 1000) + // Always request addresses — critical for Tor-only small networks { pfrom->PushMessage("getaddr"); pfrom->fGetAddr = true; @@ -4474,6 +4473,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) addrman.Add(addrFrom, addrFrom); addrman.Good(addrFrom); } + // Also request addresses from inbound peers (small network optimization) + if (!pfrom->fGetAddr) { + pfrom->PushMessage("getaddr"); + pfrom->fGetAddr = true; + } } // Ask connected nodes for block updates. @@ -5408,22 +5412,38 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { uint64_t nonce = 0; vRecv >> nonce; - // Echo the message back with the nonce. This allows for two useful features: - // - // 1) A remote node can quickly check if the connection is operational - // 2) Remote nodes can measure the latency of the network thread. If this node - // is overloaded it won't respond to pings quickly and the remote node can - // avoid sending us more work, like chain download requests. - // - // The nonce stops the remote getting confused between different pings: without - // it, if the remote node sends a ping once per second and this node takes 5 - // seconds to respond to each, the 5th ping the remote sends would appear to - // return very quickly. pfrom->PushMessage("pong", nonce); } } + else if (strCommand == "pong") + { + if (pfrom->nVersion > BIP0031_VERSION) + { + uint64_t nonce = 0; + vRecv >> nonce; + // Only accept pong if it matches our outstanding ping nonce + if (nonce != 0 && nonce == pfrom->nPingNonceSent) { + int64_t nRtt = GetTimeMicros() - pfrom->nPingUsecStart; + if (nRtt > 0) { + pfrom->nPingUsecTime = nRtt; + // Update rolling average block latency if not set + if (pfrom->nAvgBlockLatencyUs == 0) + pfrom->nAvgBlockLatencyUs = nRtt; + else + pfrom->nAvgBlockLatencyUs = (pfrom->nAvgBlockLatencyUs * 3 + nRtt) / 4; + } + pfrom->nPingNonceSent = 0; + pfrom->nPingUsecStart = 0; + pfrom->nPingRetryCount = 0; + if (fDebug) + printf("pong from %s: %.1fms\n", pfrom->addr.ToString().c_str(), (double)nRtt / 1000.0); + } + } + } + + else if (strCommand == "alert") { CAlert alert; @@ -5563,7 +5583,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) // Update the last seen time for this node's address if (pfrom->fNetworkNode) - if (strCommand == "version" || strCommand == "addr" || strCommand == "inv" || strCommand == "getdata" || strCommand == "ping") + if (strCommand == "version" || strCommand == "addr" || strCommand == "inv" || strCommand == "getdata" || strCommand == "ping" || strCommand == "pong") AddressCurrentlyConnected(pfrom->addr); @@ -5691,22 +5711,48 @@ bool SendMessages(CNode* pto, bool fSendTrickle) if (pto->nVersion == 0) return true; - // Keep-alive ping. We send a nonce of zero because we don't use it anywhere - // right now. - if (pto->nLastSend && GetTime() - pto->nLastSend > 30 * 60 && pto->ssSend.empty()) { - uint64_t nonce = 0; - if (pto->nVersion > BIP0031_VERSION) - pto->PushMessage("ping", nonce); - else - pto->PushMessage("ping"); + // Keep-alive ping every 2 minutes (critical for Tor connections that + // can be silently dropped). Also measures round-trip latency. + { + bool fPingNeeded = false; + // Send ping every 2 minutes if no recent send activity + if (pto->nLastSend && GetTime() - pto->nLastSend > 120 && pto->ssSend.empty()) + fPingNeeded = true; + // Also ping if we haven't sent one in 2 minutes regardless + if (pto->nPingUsecStart == 0 && GetTime() - pto->nTimeConnected > 120) + fPingNeeded = true; + if (pto->nPingUsecStart > 0 && GetTimeMicros() - pto->nPingUsecStart > 120 * 1000000) + fPingNeeded = true; // outstanding ping timed out, retry + + if (fPingNeeded) { + // Check for dead peer: 3 consecutive unanswered pings = disconnect + if (pto->nPingNonceSent != 0 && pto->nPingUsecStart > 0) { + pto->nPingRetryCount++; + if (pto->nPingRetryCount >= 3) { + printf("ping timeout: %s (no pong for %d pings, %.1fs)\n", + pto->addr.ToString().c_str(), pto->nPingRetryCount, + (double)(GetTimeMicros() - pto->nPingUsecStart) / 1000000.0); + pto->fDisconnect = true; + } + } + if (!pto->fDisconnect) { + uint64_t nonce = 0; + while (nonce == 0) + RAND_bytes((unsigned char*)&nonce, sizeof(nonce)); + pto->nPingNonceSent = nonce; + pto->nPingUsecStart = GetTimeMicros(); + pto->PushMessage("ping", nonce); + } + } } // Resend wallet transactions that haven't gotten in a block yet ResendWalletTransactions(); - // Address refresh broadcast + // Address refresh broadcast — every hour for small Tor-only networks + // (was 24 hours, but small networks need faster address propagation) static int64_t nLastRebroadcast; - if (!IsInitialBlockDownload() && (GetTime() - nLastRebroadcast > 24 * 60 * 60)) + if (!IsInitialBlockDownload() && (GetTime() - nLastRebroadcast > 60 * 60)) { { LOCK(cs_vNodes); @@ -5723,6 +5769,14 @@ bool SendMessages(CNode* pto, bool fSendTrickle) if (addr.IsRoutable()) pnode->PushAddress(addr); } + + // Periodically re-request addresses (every hour) + // Helps small networks discover all peers faster + if (!pnode->fGetAddr && pnode->fSuccessfullyConnected) + { + pnode->PushMessage("getaddr"); + pnode->fGetAddr = true; + } } } nLastRebroadcast = GetTime(); diff --git a/src/net.cpp b/src/net.cpp index 4dfa248..05516b1 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -37,7 +37,7 @@ extern "C" { // int tor_main(int argc, char *argv[]); } -static const int MAX_OUTBOUND_CONNECTIONS = 16; +static const int MAX_OUTBOUND_CONNECTIONS = 8; // reduced from 16 for Tor-only small networks void ThreadMessageHandler2(void* parg); void ThreadSocketHandler2(void* parg); @@ -690,6 +690,9 @@ void CNode::copyStats(CNodeStats &stats) X(fInbound); X(nStartingHeight); X(nMisbehavior); + X(nPingUsecTime); + X(nBlocksDelivered); + X(nAvgBlockLatencyUs); } #undef X @@ -1029,10 +1032,6 @@ void ThreadSocketHandler2(void* parg) if (nErr != WSAEWOULDBLOCK) printf("socket error accept failed: %d\n", nErr); } - else if (nInbound >= GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS) - { - closesocket(hSocket); - } else if (CNode::IsBanned(addr)) { printf("connection from %s dropped (banned)\n", addr.ToString().c_str()); @@ -1040,12 +1039,36 @@ void ThreadSocketHandler2(void* parg) } else { - printf("accepted connection %s\n", addr.ToString().c_str()); - CNode* pnode = new CNode(hSocket, addr, "", true); - pnode->AddRef(); - { - LOCK(cs_vNodes); - vNodes.push_back(pnode); + int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS; + bool fAccept = (nInbound < nMaxInbound); + + // Reserve 2 extra inbound slots for known seed nodes + if (!fAccept) { + bool fIsSeed = false; + static const char *(*strOnionSeedCheck)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed; + std::string incomingAddr = addr.ToStringIP(); + for (unsigned int si = 0; strOnionSeedCheck[si][0] != NULL; si++) { + if (incomingAddr.find(strOnionSeedCheck[si][0]) != std::string::npos) { + fIsSeed = true; + break; + } + } + if (fIsSeed && nInbound < nMaxInbound + 2) { + fAccept = true; + printf("accepted seed node %s (reserved slot)\n", addr.ToString().c_str()); + } + } + + if (fAccept) { + printf("accepted connection %s\n", addr.ToString().c_str()); + CNode* pnode = new CNode(hSocket, addr, "", true); + pnode->AddRef(); + { + LOCK(cs_vNodes); + vNodes.push_back(pnode); + } + } else { + closesocket(hSocket); } } } @@ -1137,14 +1160,14 @@ void ThreadSocketHandler2(void* parg) printf("socket no message in first 60 seconds, %d %d\n", pnode->nLastRecv != 0, pnode->nLastSend != 0); pnode->fDisconnect = true; } - else if (GetTime() - pnode->nLastSend > 90*60 && GetTime() - pnode->nLastSendEmpty > 90*60) + else if (GetTime() - pnode->nLastSend > 10*60 && GetTime() - pnode->nLastSendEmpty > 10*60) { - printf("socket not sending\n"); + printf("socket not sending (10min timeout)\n"); pnode->fDisconnect = true; } - else if (GetTime() - pnode->nLastRecv > 90*60) + else if (GetTime() - pnode->nLastRecv > 10*60) { - printf("socket inactivity timeout\n"); + printf("socket inactivity timeout (10min)\n"); pnode->fDisconnect = true; } } @@ -1432,16 +1455,12 @@ void ThreadOnionSeed(void* parg) printf("ThreadOnionSeed: initial seeding complete\n"); // Periodic re-seeding for isolated or under-connected nodes. - // Check every 2 minutes, re-seed when < 2 outbound peers. - // First re-seed after 5 min cooldown, then 15 min for subsequent. + // EMERGENCY MODE: When 0 outbound peers, check every 15 seconds + // NORMAL MODE: Check every 2 minutes, re-seed when < 2 outbound peers int64_t nLastReseed = GetTime(); bool bFirstReseed = true; while (!fShutdown) { - for (int i = 0; i < 120 && !fShutdown; i++) // sleep 2 minutes - MilliSleep(1000); - - if (fShutdown) break; - + // Count outbound peers to determine check interval int nOutbound = 0; { LOCK(cs_vNodes); @@ -1450,12 +1469,44 @@ void ThreadOnionSeed(void* parg) nOutbound++; } - int64_t nCooldown = bFirstReseed ? 5 * 60 : 15 * 60; + // Emergency mode: 0 peers = check every 15 seconds + // Low mode: 1 peer = check every 30 seconds + // Normal: 2+ peers = check every 2 minutes + int nSleepSeconds = (nOutbound == 0) ? 15 : (nOutbound < 2) ? 30 : 120; + for (int i = 0; i < nSleepSeconds && !fShutdown; i++) + MilliSleep(1000); + + if (fShutdown) break; + + // Recount after sleep + nOutbound = 0; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + if (!pnode->fInbound) + nOutbound++; + } + + // Emergency (0 peers): no cooldown, reseed immediately + // Low (1 peer): 60 second cooldown + // Normal (<2): 5 min first, 15 min subsequent + int64_t nCooldown; + if (nOutbound == 0) + nCooldown = 0; // immediate + else if (nOutbound < 2) + nCooldown = bFirstReseed ? 60 : 5 * 60; + else + nCooldown = bFirstReseed ? 5 * 60 : 15 * 60; + if (nOutbound < 2 && GetTime() - nLastReseed > nCooldown) { - printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound); + if (nOutbound == 0) + printf("ThreadOnionSeed: EMERGENCY - 0 outbound peers, re-seeding immediately!\n"); + else + printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound); + ThreadHTTPSeedFetch2(NULL); - // Also re-queue hardcoded seeds for direct connection + // Re-queue hardcoded seeds for direct connection for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != NULL; seed_idx++) { std::string oneShotAddr = std::string(strOnionSeed[seed_idx][0]) + ":" + std::to_string(GetDefaultPort()); @@ -2341,8 +2392,11 @@ void StartNode(void* parg) RenameThread("Triangles-start"); if (semOutbound == NULL) { - // initialize semaphore - int nMaxOutbound = min(MAX_OUTBOUND_CONNECTIONS, (int)GetArg("-maxconnections", 125)); + // initialize semaphore — use -maxoutbound if specified, else default + int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS); + nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125)); + nMaxOutbound = max(nMaxOutbound, 1); // at least 1 outbound + printf("Max outbound connections: %d\n", nMaxOutbound); semOutbound = new CSemaphore(nMaxOutbound); } @@ -2412,9 +2466,13 @@ bool StopNode() fShutdown = true; nTransactionsUpdated++; int64_t nStart = GetTime(); - if (semOutbound) - for (int i=0; ipost(); + } do { int nThreadsRunning = 0; diff --git a/src/net.h b/src/net.h index fecad0b..3e95882 100644 --- a/src/net.h +++ b/src/net.h @@ -146,6 +146,9 @@ public: bool fInbound; int nStartingHeight; int nMisbehavior; + int64_t nPingUsecTime; + int nBlocksDelivered; + int64_t nAvgBlockLatencyUs; }; @@ -279,6 +282,12 @@ public: int nBestKnownHeight; // highest block height known to this peer (updated from inv/block msgs) int nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection) + // BIP 31 ping/pong latency tracking + uint64_t nPingNonceSent; // nonce of last ping sent (0 = no outstanding ping) + int64_t nPingUsecStart; // microsecond timestamp when last ping was sent + int64_t nPingUsecTime; // last measured round-trip time (microseconds), 0 = unknown + int nPingRetryCount; // consecutive pings without pong response + // flood relay std::vector vAddrToSend; mruset setAddrKnown; @@ -331,6 +340,10 @@ public: nBlocksDelivered = 0; nBestKnownHeight = -1; nIncompatibleGetblocks = 0; + nPingNonceSent = 0; + nPingUsecStart = 0; + nPingUsecTime = 0; + nPingRetryCount = 0; fGetAddr = false; nMisbehavior = 0; hashCheckpointKnown = 0; diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 81eaf1b..d5b29b2 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -97,6 +97,9 @@ Value getpeerinfo(const Array& params, bool fHelp) obj.push_back(Pair("inbound", stats.fInbound)); obj.push_back(Pair("startingheight", stats.nStartingHeight)); obj.push_back(Pair("banscore", stats.nMisbehavior)); + obj.push_back(Pair("pingtime", stats.nPingUsecTime > 0 ? (double)stats.nPingUsecTime / 1000000.0 : -1.0)); + obj.push_back(Pair("blocksdelivered", stats.nBlocksDelivered)); + obj.push_back(Pair("avglatency", stats.nAvgBlockLatencyUs > 0 ? (double)stats.nAvgBlockLatencyUs / 1000.0 : -1.0)); ret.push_back(obj); } @@ -264,3 +267,85 @@ Value getseedlist(const Array& params, bool fHelp) return ret; } + +Value getnetworkstability(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 0) + throw runtime_error( + "getnetworkstability\n" + "Returns detailed network stability metrics including peer quality,\n" + "connection health, and isolation risk assessment."); + + int nOutbound = 0, nInbound = 0, nTotal = 0; + int64_t nBestPing = INT64_MAX, nWorstPing = 0, nTotalPing = 0; + int nPingCount = 0; + int nTotalBlocksDelivered = 0; + int64_t nOldestConnection = 0; + int64_t nNewestConnection = INT64_MAX; + + { + LOCK(cs_vNodes); + nTotal = vNodes.size(); + for (CNode* pnode : vNodes) { + if (pnode->fInbound) + nInbound++; + else + nOutbound++; + + if (pnode->nPingUsecTime > 0) { + nTotalPing += pnode->nPingUsecTime; + nPingCount++; + if (pnode->nPingUsecTime < nBestPing) + nBestPing = pnode->nPingUsecTime; + if (pnode->nPingUsecTime > nWorstPing) + nWorstPing = pnode->nPingUsecTime; + } + + nTotalBlocksDelivered += pnode->nBlocksDelivered; + + int64_t uptime = GetTime() - pnode->nTimeConnected; + if (uptime > nOldestConnection) + nOldestConnection = uptime; + if (uptime < nNewestConnection) + nNewestConnection = uptime; + } + } + + // Determine isolation risk + string strRisk; + if (nOutbound == 0 && nInbound == 0) + strRisk = "critical"; + else if (nOutbound == 0) + strRisk = "high"; + else if (nOutbound == 1) + strRisk = "elevated"; + else if (nOutbound < 3) + strRisk = "moderate"; + else + strRisk = "low"; + + Object obj; + obj.push_back(Pair("connections_total", nTotal)); + obj.push_back(Pair("connections_outbound", nOutbound)); + obj.push_back(Pair("connections_inbound", nInbound)); + obj.push_back(Pair("isolation_risk", strRisk)); + obj.push_back(Pair("blocks_delivered_total", nTotalBlocksDelivered)); + obj.push_back(Pair("known_addresses", (int)addrman.size())); + + Object pingObj; + pingObj.push_back(Pair("best_ms", nPingCount > 0 ? (double)nBestPing / 1000.0 : -1.0)); + pingObj.push_back(Pair("worst_ms", nPingCount > 0 ? (double)nWorstPing / 1000.0 : -1.0)); + pingObj.push_back(Pair("avg_ms", nPingCount > 0 ? (double)nTotalPing / nPingCount / 1000.0 : -1.0)); + pingObj.push_back(Pair("peers_measured", nPingCount)); + obj.push_back(Pair("ping", pingObj)); + + Object uptimeObj; + uptimeObj.push_back(Pair("newest_sec", nTotal > 0 ? (boost::int64_t)nNewestConnection : 0)); + uptimeObj.push_back(Pair("oldest_sec", nTotal > 0 ? (boost::int64_t)nOldestConnection : 0)); + obj.push_back(Pair("connection_uptime", uptimeObj)); + + obj.push_back(Pair("seconds_since_last_block", (boost::int64_t)(GetTime() - nTimeBestReceived))); + obj.push_back(Pair("current_height", nBestHeight)); + + return obj; +} diff --git a/src/trianglesrpc.cpp b/src/trianglesrpc.cpp index b81800d..8ab3712 100644 --- a/src/trianglesrpc.cpp +++ b/src/trianglesrpc.cpp @@ -256,6 +256,7 @@ static const CRPCCommand vRPCCommands[] = { "getwalletinfo", &getwalletinfo, true, false }, { "getnetworkinfo", &getnetworkinfo, true, false }, { "getseedlist", &getseedlist, true, false }, + { "getnetworkstability", &getnetworkstability, true, false }, { "gettxoutsetinfo", &gettxoutsetinfo, true, false }, { "estimatefee", &estimatefee, true, false }, { "getaddressbalance", &getaddressbalance, true, false }, diff --git a/src/trianglesrpc.h b/src/trianglesrpc.h index 1e72af2..c1a2296 100644 --- a/src/trianglesrpc.h +++ b/src/trianglesrpc.h @@ -148,6 +148,7 @@ extern json_spirit::Value getconnectioncount(const json_spirit::Array& params, b extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value getnetworkstability(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value addnode(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value disconnectnode(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp);