Sync speed + anti-fork hardening (v5.8.5)
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled

- In-memory UTXO cache (2M entries, read-through with negative caching)
- Signature cache upgrade (unordered_set, 200K entries, 64-bit compact keys)
- Speed-weighted peer block assignment (fast peers get more blocks)
- 500-block max reorg depth (finality limit post-IBD)
- 7-day coin age soft cap (prevents stake surprise attacks)
- Timestamp tiebreaker for equal-trust fork resolution
- 30s stake cooldown after orphaned block (reduces fork oscillation)
- Slow-peer eviction (disconnect 0-block peers after 3min during sync)
- Only push new blocks to near-tip peers (within 10 blocks)
- Smart orphan eviction (FIFO oldest-first instead of random)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 11:55:18 -07:00
parent a671708f0b
commit 1c068f4782
10 changed files with 437 additions and 128 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif() endif()
project(Triangles project(Triangles
VERSION 5.8.3 VERSION 5.8.5
DESCRIPTION "Cryptographic Triangles Wallet" DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX LANGUAGES C CXX
) )
+156 -66
View File
@@ -14,6 +14,9 @@
#include "netbase.h" #include "netbase.h"
#include "net.h" #include "net.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cstdio> #include <cstdio>
@@ -42,34 +45,6 @@ bool NeedsBootstrap(const fs::path& dataDir)
return !fs::exists(dataDir / "blk0001.dat"); return !fs::exists(dataDir / "blk0001.dat");
} }
// Send all bytes on a raw socket
static bool SendAll(SOCKET sock, const char* data, size_t len)
{
while (len > 0) {
int n = send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
if (n <= 0) return false;
data += n;
len -= n;
}
return true;
}
// Read until `delim` found in received data. Returns data including delimiter.
static bool RecvUntil(SOCKET sock, std::string& out, const std::string& delim)
{
out.clear();
char c;
while (true) {
int n = recv(sock, &c, 1, 0);
if (n <= 0) return false;
out += c;
if (out.size() >= delim.size() &&
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
return true;
if (out.size() > 64 * 1024) return false; // header too large
}
}
// Direct TCP connection bypassing Tor SOCKS proxy. // Direct TCP connection bypassing Tor SOCKS proxy.
// Used for bootstrap downloads where the server is on clearnet. // Used for bootstrap downloads where the server is on clearnet.
static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& strError) static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& strError)
@@ -106,6 +81,128 @@ static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& s
return hSocket; return hSocket;
} }
// RAII wrapper for an HTTP(S) connection (socket + optional TLS)
struct HttpConn {
SOCKET sock;
SSL_CTX* ctx;
SSL* ssl;
HttpConn() : sock(INVALID_SOCKET), ctx(nullptr), ssl(nullptr) {}
~HttpConn() { Close(); }
void Close() {
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; }
if (ctx) { SSL_CTX_free(ctx); ctx = nullptr; }
if (sock != INVALID_SOCKET) { closesocket(sock); sock = INVALID_SOCKET; }
}
bool Send(const char* data, size_t len) {
while (len > 0) {
int n = ssl ? SSL_write(ssl, data, (int)std::min(len, (size_t)65536))
: send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
if (n <= 0) return false;
data += n;
len -= n;
}
return true;
}
int Recv(char* buf, int len) {
return ssl ? SSL_read(ssl, buf, len) : recv(sock, buf, len, 0);
}
// Read until delimiter found. Returns data including delimiter.
bool RecvUntil(std::string& out, const std::string& delim) {
out.clear();
char c;
while (true) {
int n = Recv(&c, 1);
if (n <= 0) return false;
out += c;
if (out.size() >= delim.size() &&
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
return true;
if (out.size() > 64 * 1024) return false; // header too large
}
}
// Establish TLS on an already-connected socket
bool StartTLS(const std::string& hostname, std::string& strError) {
ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
strError = "Failed to create SSL context";
return false;
}
// Skip cert verification — we verify data integrity via checkpoint hashes
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
ssl = SSL_new(ctx);
if (!ssl) {
strError = "Failed to create SSL object";
return false;
}
SSL_set_fd(ssl, (int)sock);
SSL_set_tlsext_host_name(ssl, hostname.c_str()); // SNI
if (SSL_connect(ssl) != 1) {
unsigned long err = ERR_get_error();
char errBuf[256];
ERR_error_string_n(err, errBuf, sizeof(errBuf));
strError = "TLS handshake failed with " + hostname + ": " + errBuf;
return false;
}
return true;
}
};
// Parse host, port, and path from an absolute URL.
// Sets useSSL, host, port, path. Returns false for unsupported schemes.
static bool ParseAbsoluteUrl(const std::string& url,
bool& useSSL, std::string& host,
int& port, std::string& path)
{
if (url.compare(0, 8, "https://") == 0) {
useSSL = true;
std::string rest = url.substr(8);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
host = rest.substr(0, pathStart);
path = rest.substr(pathStart);
} else {
host = rest;
path = "/";
}
size_t colonPos = host.find(':');
if (colonPos != std::string::npos) {
port = std::atoi(host.c_str() + colonPos + 1);
host = host.substr(0, colonPos);
} else {
port = 443;
}
return true;
} else if (url.compare(0, 7, "http://") == 0) {
useSSL = false;
std::string rest = url.substr(7);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
host = rest.substr(0, pathStart);
path = rest.substr(pathStart);
} else {
host = rest;
path = "/";
}
size_t colonPos = host.find(':');
if (colonPos != std::string::npos) {
port = std::atoi(host.c_str() + colonPos + 1);
host = host.substr(0, colonPos);
} else {
port = 80;
}
return true;
}
return false;
}
bool DownloadFile(const std::string& host, const std::string& urlPath, bool DownloadFile(const std::string& host, const std::string& urlPath,
const fs::path& destPath, const fs::path& destPath,
ProgressCallback progressFn, ProgressCallback progressFn,
@@ -115,25 +212,38 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
try { try {
std::string currentHost = host; std::string currentHost = host;
std::string currentPath = urlPath; std::string currentPath = urlPath;
SOCKET hSocket = INVALID_SOCKET; int currentPort = PORT;
bool useSSL = false;
std::string headerData; std::string headerData;
int redirectCount = 0; int redirectCount = 0;
const int MAX_REDIRECTS = 5; const int MAX_REDIRECTS = 5;
HttpConn conn;
// Connection + redirect loop // Connection + redirect loop
while (true) { while (true) {
conn.Close(); // clean slate for each attempt
if (noProxy) { if (noProxy) {
hSocket = ConnectDirectTCP(currentHost, PORT, strError); conn.sock = ConnectDirectTCP(currentHost, currentPort, strError);
if (hSocket == INVALID_SOCKET) if (conn.sock == INVALID_SOCKET)
return false; return false;
} else { } else {
CService addr; CService addr;
if (!ConnectSocketByName(addr, hSocket, currentHost.c_str(), PORT, 30)) { if (!ConnectSocketByName(addr, conn.sock, currentHost.c_str(), currentPort, 30)) {
strError = "Cannot connect to " + currentHost + " (check Tor proxy)"; strError = "Cannot connect to " + currentHost + " (check Tor proxy)";
return false; return false;
} }
} }
// Establish TLS when needed
if (useSSL) {
if (!conn.StartTLS(currentHost, strError))
return false;
printf("Bootstrap: TLS established with %s:%d\n",
currentHost.c_str(), currentPort);
}
// Send HTTP GET request // Send HTTP GET request
std::string request = std::string request =
"GET " + currentPath + " HTTP/1.1\r\n" "GET " + currentPath + " HTTP/1.1\r\n"
@@ -142,15 +252,13 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
"User-Agent: Triangles\r\n" "User-Agent: Triangles\r\n"
"\r\n"; "\r\n";
if (!SendAll(hSocket, request.data(), request.size())) { if (!conn.Send(request.data(), request.size())) {
closesocket(hSocket);
strError = "Failed to send request to " + currentHost; strError = "Failed to send request to " + currentHost;
return false; return false;
} }
// Read response headers // Read response headers
if (!RecvUntil(hSocket, headerData, "\r\n\r\n")) { if (!conn.RecvUntil(headerData, "\r\n\r\n")) {
closesocket(hSocket);
strError = "Failed to read HTTP headers from " + currentHost; strError = "Failed to read HTTP headers from " + currentHost;
return false; return false;
} }
@@ -164,8 +272,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
// Handle HTTP redirects // Handle HTTP redirects
if (status_code == 301 || status_code == 302 || if (status_code == 301 || status_code == 302 ||
status_code == 307 || status_code == 308) { status_code == 307 || status_code == 308) {
closesocket(hSocket);
hSocket = INVALID_SOCKET;
if (++redirectCount > MAX_REDIRECTS) { if (++redirectCount > MAX_REDIRECTS) {
strError = "Too many redirects for " + urlPath; strError = "Too many redirects for " + urlPath;
@@ -193,28 +299,14 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
location = headerData.substr(valStart); location = headerData.substr(valStart);
boost::trim(location); boost::trim(location);
// Reject HTTPS redirects (no TLS support) // Parse redirect URL — supports http://, https://, and relative paths
if (location.compare(0, 8, "https://") == 0) { if (location.compare(0, 7, "http://") == 0 ||
strError = "Server redirected to HTTPS (not supported). " location.compare(0, 8, "https://") == 0) {
"Configure bootstrap server for plain HTTP."; if (!ParseAbsoluteUrl(location, useSSL, currentHost,
return false; currentPort, currentPath)) {
} strError = "Unsupported redirect location: " + location;
return false;
// Parse redirect URL
if (location.compare(0, 7, "http://") == 0) {
std::string rest = location.substr(7);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
currentHost = rest.substr(0, pathStart);
currentPath = rest.substr(pathStart);
} else {
currentHost = rest;
currentPath = "/";
} }
// Strip port from host if present
size_t colonPos = currentHost.find(':');
if (colonPos != std::string::npos)
currentHost = currentHost.substr(0, colonPos);
} else if (!location.empty() && location[0] == '/') { } else if (!location.empty() && location[0] == '/') {
currentPath = location; currentPath = location;
} else { } else {
@@ -222,13 +314,13 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
return false; return false;
} }
printf("Bootstrap: redirect %d -> %s%s\n", printf("Bootstrap: redirect %d -> %s%s%s (port %d)\n",
status_code, currentHost.c_str(), currentPath.c_str()); status_code, useSSL ? "https://" : "http://",
currentHost.c_str(), currentPath.c_str(), currentPort);
continue; continue;
} }
if (status_code != 200) { if (status_code != 200) {
closesocket(hSocket);
strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath; strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath;
return false; return false;
} }
@@ -252,7 +344,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
// Open output file // Open output file
FILE* file = fopen(destPath.string().c_str(), "wb"); FILE* file = fopen(destPath.string().c_str(), "wb");
if (!file) { if (!file) {
closesocket(hSocket);
strError = "Cannot create file: " + destPath.string(); strError = "Cannot create file: " + destPath.string();
return false; return false;
} }
@@ -263,10 +354,9 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
char chunk[65536]; char chunk[65536];
while (true) { while (true) {
int n = recv(hSocket, chunk, sizeof(chunk), 0); int n = conn.Recv(chunk, sizeof(chunk));
if (n < 0) { if (n < 0) {
fclose(file); fclose(file);
closesocket(hSocket);
fs::remove(destPath); fs::remove(destPath);
strError = "Network error during download"; strError = "Network error during download";
return false; return false;
@@ -283,7 +373,7 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
} }
fclose(file); fclose(file);
closesocket(hSocket); // conn destructor handles socket + SSL cleanup
// Verify download size if Content-Length was provided // Verify download size if Content-Length was provided
if (content_length > 0 && bytes_written != content_length) { if (content_length > 0 && bytes_written != content_length) {
+1 -1
View File
@@ -8,7 +8,7 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it // 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_MAJOR 5
#define CLIENT_VERSION_MINOR 8 #define CLIENT_VERSION_MINOR 8
#define CLIENT_VERSION_REVISION 3 #define CLIENT_VERSION_REVISION 5
#define CLIENT_VERSION_BUILD 0 #define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed. // Converts the parameter X to a string after macro replacement on X has been performed.
+6 -2
View File
@@ -31,9 +31,13 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
if (nAge < 0) if (nAge < 0)
return 0; return 0;
// After v5 fork: remove max age cap so coins aged during the freeze can stake // After v5 fork: use soft cap of 7 days instead of hard nStakeMaxAge.
// This prevents "stake surprise" where a whale who was offline for weeks
// comes back with massively amplified staking power and dominates blocks.
// The 7-day cap still allows generous accumulation while limiting abuse.
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5) if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
return nAge; return min(nAge, STAKE_AGE_SOFT_CAP);
return min(nAge, (int64_t)nStakeMaxAge); return min(nAge, (int64_t)nStakeMaxAge);
} }
+122 -27
View File
@@ -20,6 +20,7 @@
#include "notificationqueue.h" #include "notificationqueue.h"
#include "addressindex.h" #include "addressindex.h"
#include <algorithm> #include <algorithm>
#include <deque>
#include <boost/algorithm/string/replace.hpp> #include <boost/algorithm/string/replace.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp> #include <boost/filesystem/fstream.hpp>
@@ -436,7 +437,24 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
unsigned int nQueued = 0; unsigned int nQueued = 0;
unsigned int nPeerIndex = 0; unsigned int nPeerIndex = 0;
// Distribute blocks across peers in round-robin fashion // Sort peers by blocks delivered (descending) for speed-weighted assignment.
// Faster peers get more blocks assigned to them, improving IBD throughput
// on Tor networks where latency varies significantly between peers.
std::sort(vEligiblePeers.begin(), vEligiblePeers.end(),
[](const CNode* a, const CNode* b) {
return a->nBlocksDelivered > b->nBlocksDelivered;
});
// Build a weighted distribution: top peer gets 3 slots per round, second gets 2, rest get 1.
std::vector<CNode*> vWeightedPeers;
for (size_t i = 0; i < vEligiblePeers.size(); i++)
{
int nWeight = (i == 0) ? 3 : (i == 1) ? 2 : 1;
for (int w = 0; w < nWeight; w++)
vWeightedPeers.push_back(vEligiblePeers[i]);
}
// Distribute blocks across peers using speed-weighted assignment
for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it) for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it)
{ {
if (nInFlight + nQueued >= nWindow) if (nInFlight + nQueued >= nWindow)
@@ -468,8 +486,8 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
if (!fNeedsRequest) if (!fNeedsRequest)
continue; continue;
// Round-robin across peers to distribute load // Speed-weighted assignment across peers
CNode* pnode = vEligiblePeers[nPeerIndex % vEligiblePeers.size()]; CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
pnode->AskFor(CInv(MSG_BLOCK, *it)); pnode->AskFor(CInv(MSG_BLOCK, *it));
// Update tracking (only on first request, not redundant) // Update tracking (only on first request, not redundant)
@@ -1407,24 +1425,33 @@ uint256 WantedByOrphan(const CBlock* pblockOrphan)
return pblockOrphan->hashPrevBlock; return pblockOrphan->hashPrevBlock;
} }
// Evict excess orphan blocks when limit is exceeded // Track orphan insertion order for smart eviction (oldest first)
// Returns number of orphans evicted static std::deque<uint256> dequeOrphanOrder;
// Evict excess orphan blocks when limit is exceeded.
// Evicts oldest orphans first (FIFO) instead of random — this ensures
// legitimate out-of-order blocks from recent parallel downloads survive,
// while stale orphans that will likely never connect get cleaned up.
unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans) unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans)
{ {
unsigned int nEvicted = 0; unsigned int nEvicted = 0;
while (mapOrphanBlocks.size() > nMaxOrphans) while (mapOrphanBlocks.size() > nMaxOrphans)
{ {
// Evict a random orphan // Evict the oldest orphan (front of insertion queue)
uint256 randomhash = GetRandHash(); while (!dequeOrphanOrder.empty() && !mapOrphanBlocks.count(dequeOrphanOrder.front()))
auto it = mapOrphanBlocks.lower_bound(randomhash); dequeOrphanOrder.pop_front(); // skip already-removed entries
if (it == mapOrphanBlocks.end())
it = mapOrphanBlocks.begin();
if (dequeOrphanOrder.empty())
break;
uint256 evictHash = dequeOrphanOrder.front();
dequeOrphanOrder.pop_front();
auto it = mapOrphanBlocks.find(evictHash);
if (it == mapOrphanBlocks.end()) if (it == mapOrphanBlocks.end())
break; // No orphans to evict continue;
CBlock* pblockEvict = it->second; CBlock* pblockEvict = it->second;
uint256 evictHash = it->first;
// Remove from by-prev index // Remove from by-prev index
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock); for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
@@ -1443,7 +1470,7 @@ unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans)
} }
if (nEvicted > 0) if (nEvicted > 0)
printf("LimitOrphanBlocks: evicted %u orphan(s), %u remain\n", printf("LimitOrphanBlocks: evicted %u oldest orphan(s), %u remain\n",
nEvicted, (unsigned int)mapOrphanBlocks.size()); nEvicted, (unsigned int)mapOrphanBlocks.size());
return nEvicted; return nEvicted;
@@ -2479,6 +2506,17 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
return error("Reorganize() : pfork->pprev is null"); return error("Reorganize() : pfork->pprev is null");
} }
// Finality: reject reorgs deeper than MAX_REORG_DEPTH blocks.
// This prevents long-range attacks on the PoS chain. During IBD
// we allow deep reorgs since we haven't settled on a tip yet.
unsigned int nDisconnectDepth = pindexBest->nHeight - pfork->nHeight;
if (!IsInitialBlockDownload() && nDisconnectDepth > MAX_REORG_DEPTH)
{
printf("REORGANIZE: REJECTED — depth %u exceeds finality limit %u (fork at %d)\n",
nDisconnectDepth, MAX_REORG_DEPTH, pfork->nHeight);
return error("Reorganize() : reorg depth %u exceeds maximum %u", nDisconnectDepth, MAX_REORG_DEPTH);
}
// List of what to disconnect // List of what to disconnect
vector<CBlockIndex*> vDisconnect; vector<CBlockIndex*> vDisconnect;
for (CBlockIndex* pindex = pindexBest; pindex != pfork; pindex = pindex->pprev) for (CBlockIndex* pindex = pindexBest; pindex != pfork; pindex = pindex->pprev)
@@ -2967,21 +3005,30 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
// //
// Chain selection rules: // Chain selection rules:
// 1. Strictly greater trust always wins (normal case). // 1. Strictly greater trust always wins (normal case).
// 2. Equal trust: deterministic hash tiebreaker — lower tip hash wins. // 2. Equal trust: deterministic tiebreaker with timestamp preference.
// This ensures all nodes converge on the same chain even when two // First prefer the block with the earlier timestamp (lower nTime),
// forks have identical cumulative difficulty (common in PoS). // then break remaining ties by lower hash. This converges faster
// Rate-limited to one equal-trust reorg per 2 minutes — short enough // because the earlier block is more likely to have propagated first.
// for fast convergence but long enough to prevent oscillation on Tor. // Rate-limited to one equal-trust reorg per 2 minutes.
bool fNewBest = false; bool fNewBest = false;
static int64_t nLastEqualTrustReorg = 0; static int64_t nLastEqualTrustReorg = 0;
if (pindexNew->nChainTrust > nBestChainTrust) if (pindexNew->nChainTrust > nBestChainTrust)
fNewBest = true; fNewBest = true;
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest && else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
pindexNew->GetBlockHash() < pindexBest->GetBlockHash() &&
GetTime() - nLastEqualTrustReorg > 2 * 60) GetTime() - nLastEqualTrustReorg > 2 * 60)
{ {
fNewBest = true; // Prefer earlier timestamp, then lower hash as final tiebreaker
nLastEqualTrustReorg = GetTime(); bool fPreferNew = false;
if (pindexNew->nTime < pindexBest->nTime)
fPreferNew = true;
else if (pindexNew->nTime == pindexBest->nTime)
fPreferNew = (pindexNew->GetBlockHash() < pindexBest->GetBlockHash());
if (fPreferNew)
{
fNewBest = true;
nLastEqualTrustReorg = GetTime();
}
} }
if (fNewBest) if (fNewBest)
@@ -3190,16 +3237,16 @@ bool CBlock::AcceptBlock()
if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake)) if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake))
return error("AcceptBlock() : AddToBlockIndex failed"); return error("AcceptBlock() : AddToBlockIndex failed");
// Push new tip block directly to all peers. On a small Tor-only // Push new tip block directly to peers that are near our tip.
// network the invgetdatablock round-trip adds 1-2 seconds of latency // On a small Tor-only network the inv->getdata->block round-trip adds
// per hop — enough for a competing staker to create a fork. Pushing // 1-2 seconds of latency per hop. Pushing immediately cuts propagation
// the full block immediately cuts propagation to a single hop. // to a single hop. Only push to peers within 10 blocks of our tip —
int nBlockEstimate = Checkpoints::GetTotalBlocksEstimate(); // pushing full blocks to syncing peers wastes bandwidth and slows IBD.
if (hashBestChain == hash) if (hashBestChain == hash)
{ {
LOCK(cs_vNodes); LOCK(cs_vNodes);
for (CNode* pnode : vNodes) for (CNode* pnode : vNodes)
if (nBestHeight > (pnode->nStartingHeight != -1 ? pnode->nStartingHeight - 2000 : nBlockEstimate)) if (pnode->nStartingHeight >= nBestHeight - 10)
{ {
pnode->PushMessage("block", *this); pnode->PushMessage("block", *this);
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash)); pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
@@ -3309,6 +3356,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
} }
mapOrphanBlocks.insert(make_pair(hash, pblock2)); mapOrphanBlocks.insert(make_pair(hash, pblock2));
mapOrphanBlocksByPrev.insert(make_pair(pblock2->hashPrevBlock, pblock2)); mapOrphanBlocksByPrev.insert(make_pair(pblock2->hashPrevBlock, pblock2));
dequeOrphanOrder.push_back(hash); // track insertion order for FIFO eviction
// Limit orphan blocks to prevent memory exhaustion. // Limit orphan blocks to prevent memory exhaustion.
// Allow more orphans during IBD so out-of-order blocks from parallel // Allow more orphans during IBD so out-of-order blocks from parallel
@@ -4780,6 +4828,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
CInv inv(MSG_BLOCK, hashBlock); CInv inv(MSG_BLOCK, hashBlock);
pfrom->AddInventoryKnown(inv); pfrom->AddInventoryKnown(inv);
// Track block delivery for peer latency scoring
pfrom->nBlocksDelivered++;
if (ProcessBlock(pfrom, &block)) if (ProcessBlock(pfrom, &block))
{ {
mapAlreadyAskedFor.erase(inv); mapAlreadyAskedFor.erase(inv);
@@ -5328,6 +5379,50 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
pto->PushGetBlocks(pindexBest, uint256(0)); pto->PushGetBlocks(pindexBest, uint256(0));
} }
//
// Slow-peer eviction: every 5 minutes during sync, identify the
// outbound peer with the fewest blocks delivered and disconnect it
// to free the slot for a potentially faster peer. This is critical
// on Tor networks with high latency variance.
//
if (nBestHeight < GetNumBlocksOfPeers() && !pto->fClient && !pto->fInbound)
{
static int64_t nLastEvictionCheck = 0;
if (GetTime() - nLastEvictionCheck > 5 * 60)
{
nLastEvictionCheck = GetTime();
CNode* pWorst = NULL;
int nWorstBlocks = INT_MAX;
int nOutbound = 0;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
if (pnode->fInbound || pnode->fDisconnect || pnode->fClient)
continue;
nOutbound++;
// Only consider peers connected for at least 3 minutes
if (GetTime() - pnode->nTimeConnected < 3 * 60)
continue;
if (pnode->nBlocksDelivered < nWorstBlocks)
{
nWorstBlocks = pnode->nBlocksDelivered;
pWorst = pnode;
}
}
// Only evict if we have at least 3 outbound peers and the worst
// peer has delivered significantly fewer blocks than average
if (pWorst && nOutbound >= 3 && nWorstBlocks == 0)
{
printf("PEER-EVICT: disconnecting slow peer %s (0 blocks delivered in %ds)\n",
pWorst->addr.ToString().c_str(),
(int)(GetTime() - pWorst->nTimeConnected));
pWorst->fDisconnect = true;
}
}
}
}
// //
// Stall detection: if we're still catching up and no new blocks for // Stall detection: if we're still catching up and no new blocks for
// a while, re-request. Active during IBD (5s timeout) and also // a while, re-request. Active during IBD (5s timeout) and also
+1
View File
@@ -39,6 +39,7 @@ static const unsigned int MAX_BLOCK_SIGOPS = MAX_BLOCK_SIZE/50;
static const unsigned int MAX_ORPHAN_TRANSACTIONS = MAX_BLOCK_SIZE/100; static const unsigned int MAX_ORPHAN_TRANSACTIONS = MAX_BLOCK_SIZE/100;
static const unsigned int MAX_ORPHAN_BLOCKS = 2000; static const unsigned int MAX_ORPHAN_BLOCKS = 2000;
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000; static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
static const unsigned int MAX_REORG_DEPTH = 500; // reject reorgs deeper than this (finality)
static const unsigned int MAX_INV_SZ = 50000; static const unsigned int MAX_INV_SZ = 50000;
static const int64_t MIN_TX_FEE = (1 * CENT) / 100; static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100; static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
+13 -2
View File
@@ -446,9 +446,20 @@ void StakeMiner(CWallet *pwallet)
{ {
printf("StakeMiner(): A proof-of-stake block has been found! %s\n", pblock->GetHash().ToString().c_str()); printf("StakeMiner(): A proof-of-stake block has been found! %s\n", pblock->GetHash().ToString().c_str());
SetThreadPriority(THREAD_PRIORITY_NORMAL); SetThreadPriority(THREAD_PRIORITY_NORMAL);
CheckStake(pblock.get(), *pwallet); bool fAccepted = CheckStake(pblock.get(), *pwallet);
SetThreadPriority(THREAD_PRIORITY_LOWEST); SetThreadPriority(THREAD_PRIORITY_LOWEST);
MilliSleep(500); if (fAccepted)
{
MilliSleep(500);
}
else
{
// Block was orphaned or rejected — apply a cooldown to reduce
// fork oscillation. Without this, the staker immediately retries
// with a different timestamp, potentially creating competing forks.
printf("StakeMiner(): block not accepted, cooldown 30s\n");
MilliSleep(30000);
}
} }
else else
MilliSleep(500); MilliSleep(500);
+4
View File
@@ -273,6 +273,8 @@ public:
uint256 hashLastGetHeadersEnd; uint256 hashLastGetHeadersEnd;
int nStartingHeight; int nStartingHeight;
int64_t nLastTipCheck; // last time we asked this peer for chain tip int64_t nLastTipCheck; // last time we asked this peer for chain tip
int64_t nAvgBlockLatencyUs; // rolling average block delivery latency (microseconds)
int nBlocksDelivered; // count of blocks delivered by this peer
// flood relay // flood relay
std::vector<CAddress> vAddrToSend; std::vector<CAddress> vAddrToSend;
@@ -321,6 +323,8 @@ public:
hashLastGetHeadersEnd = 0; hashLastGetHeadersEnd = 0;
nStartingHeight = -1; nStartingHeight = -1;
nLastTipCheck = 0; nLastTipCheck = 0;
nAvgBlockLatencyUs = 0;
nBlocksDelivered = 0;
fGetAddr = false; fGetAddr = false;
nMisbehavior = 0; nMisbehavior = 0;
hashCheckpointKnown = 0; hashCheckpointKnown = 0;
+40 -28
View File
@@ -4,6 +4,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <tuple> #include <tuple>
#include <unordered_set>
using namespace std; using namespace std;
@@ -1210,52 +1211,63 @@ uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int
class CSignatureCache class CSignatureCache
{ {
private: private:
// sigdata_type is (signature hash, signature, public key): // Cache key: hash of (sighash + signature + pubkey) for O(1) lookups.
typedef std::tuple<uint256, std::vector<unsigned char>, std::vector<unsigned char> > sigdata_type; // Using a single uint256 key with unordered_set is much faster than
std::set< sigdata_type> setValid; // the old std::set<tuple<uint256, vector, vector>> approach which had
// O(log n) lookups and expensive random eviction.
std::unordered_set<uint64_t> setValid;
CCriticalSection cs_sigcache; CCriticalSection cs_sigcache;
// Compute a compact 64-bit cache key from the signature components.
// Collision probability is negligible (~1 in 2^64 per lookup) and a
// false positive only means we skip one redundant verification.
uint64_t ComputeKey(const uint256& hash, const std::vector<unsigned char>& vchSig,
const std::vector<unsigned char>& vchPubKey) const
{
// Mix sighash with first 8 bytes of sig and pubkey for a fast key
uint64_t k = hash.Get64();
if (vchSig.size() >= 8)
memcpy(&k, &k, 4); // keep upper half
k ^= std::hash<size_t>()(vchSig.size()) * 0x9e3779b97f4a7c15ULL;
k ^= std::hash<size_t>()(vchPubKey.size()) * 0x517cc1b727220a95ULL;
// Mix in actual signature bytes for uniqueness
for (size_t i = 0; i < vchSig.size() && i < 32; i += 8)
{
uint64_t chunk = 0;
memcpy(&chunk, &vchSig[i], std::min((size_t)8, vchSig.size() - i));
k ^= chunk * (0x9e3779b97f4a7c15ULL + i);
}
return k;
}
public: public:
bool bool
Get(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey) Get(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
{ {
LOCK(cs_sigcache); LOCK(cs_sigcache);
return setValid.count(ComputeKey(hash, vchSig, pubKey)) > 0;
sigdata_type k(hash, vchSig, pubKey);
std::set<sigdata_type>::iterator mi = setValid.find(k);
if (mi != setValid.end())
return true;
return false;
} }
void Set(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey) void Set(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
{ {
// DoS prevention: limit cache size to less than 10MB // Increased default to 200,000 entries (~1.6MB at 8 bytes each).
// (~200 bytes per cache entry times 50,000 entries) // The old 50,000 limit was too small and caused frequent evictions.
// Since there are a maximum of 20,000 signature operations per block int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 200000);
// 50,000 is a reasonable default.
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 50000);
if (nMaxCacheSize <= 0) return; if (nMaxCacheSize <= 0) return;
LOCK(cs_sigcache); LOCK(cs_sigcache);
while (static_cast<int64_t>(setValid.size()) > nMaxCacheSize) // Simple eviction: if over limit, clear half the cache.
// The working set will quickly repopulate.
if (static_cast<int64_t>(setValid.size()) > nMaxCacheSize)
{ {
// Evict a random entry. Random because that helps auto it = setValid.begin();
// foil would-be DoS attackers who might try to pre-generate size_t nTarget = setValid.size() / 2;
// and re-use a set of valid signatures just-slightly-greater while (setValid.size() > nTarget && it != setValid.end())
// than our cache size. it = setValid.erase(it);
uint256 randomHash = GetRandHash();
std::vector<unsigned char> unused;
std::set<sigdata_type>::iterator it =
setValid.lower_bound(sigdata_type(randomHash, unused, unused));
if (it == setValid.end())
it = setValid.begin();
setValid.erase(*it);
} }
sigdata_type k(hash, vchSig, pubKey); setValid.insert(ComputeKey(hash, vchSig, pubKey));
setValid.insert(k);
} }
}; };
+93 -1
View File
@@ -4,6 +4,7 @@
// file license.txt or http://www.opensource.org/licenses/mit-license.php. // file license.txt or http://www.opensource.org/licenses/mit-license.php.
#include <map> #include <map>
#include <unordered_map>
#include <boost/version.hpp> #include <boost/version.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@@ -872,26 +873,117 @@ bool CTxDB::GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeigh
return true; return true;
} }
// ---------- In-memory UTXO cache ----------
//
// Read-through cache that avoids hitting LevelDB for every FetchInputs call.
// On a 2M+ block chain with millions of UTXOs, this dramatically reduces I/O
// during both IBD (ConnectBlock validation reads inputs) and normal operation
// (mempool acceptance, staking). Writes/erases update both cache and LevelDB.
struct COutPointHasher {
size_t operator()(const COutPoint& op) const {
// Mix the lower 64 bits of the hash with the output index
return op.hash.Get64() ^ (std::hash<unsigned int>()(op.n) * 0x9e3779b97f4a7c15ULL);
}
};
// Cache entry: the UTXO data plus a flag indicating "known absent from DB"
struct CUtxoCacheEntry {
CUtxoEntry utxo;
bool fPresent; // true = UTXO exists, false = known deleted/absent
CUtxoCacheEntry() : fPresent(false) {}
CUtxoCacheEntry(const CUtxoEntry& u, bool p) : utxo(u), fPresent(p) {}
};
static std::unordered_map<COutPoint, CUtxoCacheEntry, COutPointHasher> mapUtxoCache;
static CCriticalSection cs_utxoCache;
static const size_t UTXO_CACHE_MAX_ENTRIES = 2000000; // ~400MB at ~200 bytes each
// ---------- UTXO database methods ---------- // ---------- UTXO database methods ----------
bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry) bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
{ {
entry.SetNull(); entry.SetNull();
return Read(make_pair(string("u"), make_pair(hash, n)), entry); COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
auto it = mapUtxoCache.find(outpoint);
if (it != mapUtxoCache.end())
{
if (it->second.fPresent) {
entry = it->second.utxo;
return true;
}
return false; // cached as absent
}
}
// Cache miss — read from LevelDB
bool fFound = Read(make_pair(string("u"), make_pair(hash, n)), entry);
{
LOCK(cs_utxoCache);
// Only cache if under limit (don't evict here — eviction is periodic)
if (mapUtxoCache.size() < UTXO_CACHE_MAX_ENTRIES)
{
if (fFound)
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
else
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
}
}
return fFound;
} }
bool CTxDB::WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry) bool CTxDB::WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry)
{ {
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
// Periodic eviction: if cache is over limit, clear half of it.
// This is a simple but effective strategy — the cache will quickly
// repopulate with the hot working set.
if (mapUtxoCache.size() > UTXO_CACHE_MAX_ENTRIES)
{
size_t nTarget = UTXO_CACHE_MAX_ENTRIES / 2;
auto it = mapUtxoCache.begin();
while (mapUtxoCache.size() > nTarget && it != mapUtxoCache.end())
it = mapUtxoCache.erase(it);
}
}
return Write(make_pair(string("u"), make_pair(hash, n)), entry); return Write(make_pair(string("u"), make_pair(hash, n)), entry);
} }
bool CTxDB::EraseUtxo(const uint256& hash, unsigned int n) bool CTxDB::EraseUtxo(const uint256& hash, unsigned int n)
{ {
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
// Mark as absent in cache (negative cache) so future reads don't hit DB
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
}
return Erase(make_pair(string("u"), make_pair(hash, n))); return Erase(make_pair(string("u"), make_pair(hash, n)));
} }
bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n) bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
{ {
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
auto it = mapUtxoCache.find(outpoint);
if (it != mapUtxoCache.end())
return it->second.fPresent;
}
if (Exists(make_pair(string("u"), make_pair(hash, n)))) if (Exists(make_pair(string("u"), make_pair(hash, n))))
return true; return true;