Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c068f4782 |
+1
-1
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
project(Triangles
|
||||
VERSION 5.8.3
|
||||
VERSION 5.8.5
|
||||
DESCRIPTION "Cryptographic Triangles Wallet"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
|
||||
+156
-66
@@ -14,6 +14,9 @@
|
||||
#include "netbase.h"
|
||||
#include "net.h"
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
@@ -42,34 +45,6 @@ bool NeedsBootstrap(const fs::path& dataDir)
|
||||
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.
|
||||
// Used for bootstrap downloads where the server is on clearnet.
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
const fs::path& destPath,
|
||||
ProgressCallback progressFn,
|
||||
@@ -115,25 +212,38 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
try {
|
||||
std::string currentHost = host;
|
||||
std::string currentPath = urlPath;
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
int currentPort = PORT;
|
||||
bool useSSL = false;
|
||||
std::string headerData;
|
||||
int redirectCount = 0;
|
||||
const int MAX_REDIRECTS = 5;
|
||||
|
||||
HttpConn conn;
|
||||
|
||||
// Connection + redirect loop
|
||||
while (true) {
|
||||
conn.Close(); // clean slate for each attempt
|
||||
|
||||
if (noProxy) {
|
||||
hSocket = ConnectDirectTCP(currentHost, PORT, strError);
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
conn.sock = ConnectDirectTCP(currentHost, currentPort, strError);
|
||||
if (conn.sock == INVALID_SOCKET)
|
||||
return false;
|
||||
} else {
|
||||
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)";
|
||||
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
|
||||
std::string request =
|
||||
"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"
|
||||
"\r\n";
|
||||
|
||||
if (!SendAll(hSocket, request.data(), request.size())) {
|
||||
closesocket(hSocket);
|
||||
if (!conn.Send(request.data(), request.size())) {
|
||||
strError = "Failed to send request to " + currentHost;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read response headers
|
||||
if (!RecvUntil(hSocket, headerData, "\r\n\r\n")) {
|
||||
closesocket(hSocket);
|
||||
if (!conn.RecvUntil(headerData, "\r\n\r\n")) {
|
||||
strError = "Failed to read HTTP headers from " + currentHost;
|
||||
return false;
|
||||
}
|
||||
@@ -164,8 +272,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
// Handle HTTP redirects
|
||||
if (status_code == 301 || status_code == 302 ||
|
||||
status_code == 307 || status_code == 308) {
|
||||
closesocket(hSocket);
|
||||
hSocket = INVALID_SOCKET;
|
||||
|
||||
if (++redirectCount > MAX_REDIRECTS) {
|
||||
strError = "Too many redirects for " + urlPath;
|
||||
@@ -193,28 +299,14 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
location = headerData.substr(valStart);
|
||||
boost::trim(location);
|
||||
|
||||
// Reject HTTPS redirects (no TLS support)
|
||||
if (location.compare(0, 8, "https://") == 0) {
|
||||
strError = "Server redirected to HTTPS (not supported). "
|
||||
"Configure bootstrap server for plain HTTP.";
|
||||
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 = "/";
|
||||
// Parse redirect URL — supports http://, https://, and relative paths
|
||||
if (location.compare(0, 7, "http://") == 0 ||
|
||||
location.compare(0, 8, "https://") == 0) {
|
||||
if (!ParseAbsoluteUrl(location, useSSL, currentHost,
|
||||
currentPort, currentPath)) {
|
||||
strError = "Unsupported redirect location: " + location;
|
||||
return false;
|
||||
}
|
||||
// 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] == '/') {
|
||||
currentPath = location;
|
||||
} else {
|
||||
@@ -222,13 +314,13 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Bootstrap: redirect %d -> %s%s\n",
|
||||
status_code, currentHost.c_str(), currentPath.c_str());
|
||||
printf("Bootstrap: redirect %d -> %s%s%s (port %d)\n",
|
||||
status_code, useSSL ? "https://" : "http://",
|
||||
currentHost.c_str(), currentPath.c_str(), currentPort);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status_code != 200) {
|
||||
closesocket(hSocket);
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath;
|
||||
return false;
|
||||
}
|
||||
@@ -252,7 +344,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
// Open output file
|
||||
FILE* file = fopen(destPath.string().c_str(), "wb");
|
||||
if (!file) {
|
||||
closesocket(hSocket);
|
||||
strError = "Cannot create file: " + destPath.string();
|
||||
return false;
|
||||
}
|
||||
@@ -263,10 +354,9 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
char chunk[65536];
|
||||
|
||||
while (true) {
|
||||
int n = recv(hSocket, chunk, sizeof(chunk), 0);
|
||||
int n = conn.Recv(chunk, sizeof(chunk));
|
||||
if (n < 0) {
|
||||
fclose(file);
|
||||
closesocket(hSocket);
|
||||
fs::remove(destPath);
|
||||
strError = "Network error during download";
|
||||
return false;
|
||||
@@ -283,7 +373,7 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
closesocket(hSocket);
|
||||
// conn destructor handles socket + SSL cleanup
|
||||
|
||||
// Verify download size if Content-Length was provided
|
||||
if (content_length > 0 && bytes_written != content_length) {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// 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 3
|
||||
#define CLIENT_VERSION_REVISION 5
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
+6
-2
@@ -31,9 +31,13 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
|
||||
if (nAge < 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)
|
||||
return nAge;
|
||||
return min(nAge, STAKE_AGE_SOFT_CAP);
|
||||
|
||||
return min(nAge, (int64_t)nStakeMaxAge);
|
||||
}
|
||||
|
||||
+122
-27
@@ -20,6 +20,7 @@
|
||||
#include "notificationqueue.h"
|
||||
#include "addressindex.h"
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
@@ -436,7 +437,24 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
|
||||
unsigned int nQueued = 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)
|
||||
{
|
||||
if (nInFlight + nQueued >= nWindow)
|
||||
@@ -468,8 +486,8 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
|
||||
if (!fNeedsRequest)
|
||||
continue;
|
||||
|
||||
// Round-robin across peers to distribute load
|
||||
CNode* pnode = vEligiblePeers[nPeerIndex % vEligiblePeers.size()];
|
||||
// Speed-weighted assignment across peers
|
||||
CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
|
||||
pnode->AskFor(CInv(MSG_BLOCK, *it));
|
||||
|
||||
// Update tracking (only on first request, not redundant)
|
||||
@@ -1407,24 +1425,33 @@ uint256 WantedByOrphan(const CBlock* pblockOrphan)
|
||||
return pblockOrphan->hashPrevBlock;
|
||||
}
|
||||
|
||||
// Evict excess orphan blocks when limit is exceeded
|
||||
// Returns number of orphans evicted
|
||||
// Track orphan insertion order for smart eviction (oldest first)
|
||||
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 nEvicted = 0;
|
||||
while (mapOrphanBlocks.size() > nMaxOrphans)
|
||||
{
|
||||
// Evict a random orphan
|
||||
uint256 randomhash = GetRandHash();
|
||||
auto it = mapOrphanBlocks.lower_bound(randomhash);
|
||||
if (it == mapOrphanBlocks.end())
|
||||
it = mapOrphanBlocks.begin();
|
||||
// Evict the oldest orphan (front of insertion queue)
|
||||
while (!dequeOrphanOrder.empty() && !mapOrphanBlocks.count(dequeOrphanOrder.front()))
|
||||
dequeOrphanOrder.pop_front(); // skip already-removed entries
|
||||
|
||||
if (dequeOrphanOrder.empty())
|
||||
break;
|
||||
|
||||
uint256 evictHash = dequeOrphanOrder.front();
|
||||
dequeOrphanOrder.pop_front();
|
||||
|
||||
auto it = mapOrphanBlocks.find(evictHash);
|
||||
if (it == mapOrphanBlocks.end())
|
||||
break; // No orphans to evict
|
||||
continue;
|
||||
|
||||
CBlock* pblockEvict = it->second;
|
||||
uint256 evictHash = it->first;
|
||||
|
||||
// Remove from by-prev index
|
||||
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
||||
@@ -1443,7 +1470,7 @@ unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans)
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
return nEvicted;
|
||||
@@ -2479,6 +2506,17 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
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
|
||||
vector<CBlockIndex*> vDisconnect;
|
||||
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:
|
||||
// 1. Strictly greater trust always wins (normal case).
|
||||
// 2. Equal trust: deterministic hash tiebreaker — lower tip hash wins.
|
||||
// This ensures all nodes converge on the same chain even when two
|
||||
// forks have identical cumulative difficulty (common in PoS).
|
||||
// Rate-limited to one equal-trust reorg per 2 minutes — short enough
|
||||
// for fast convergence but long enough to prevent oscillation on Tor.
|
||||
// 2. Equal trust: deterministic tiebreaker with timestamp preference.
|
||||
// First prefer the block with the earlier timestamp (lower nTime),
|
||||
// then break remaining ties by lower hash. This converges faster
|
||||
// because the earlier block is more likely to have propagated first.
|
||||
// Rate-limited to one equal-trust reorg per 2 minutes.
|
||||
bool fNewBest = false;
|
||||
static int64_t nLastEqualTrustReorg = 0;
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
fNewBest = true;
|
||||
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
|
||||
pindexNew->GetBlockHash() < pindexBest->GetBlockHash() &&
|
||||
GetTime() - nLastEqualTrustReorg > 2 * 60)
|
||||
{
|
||||
fNewBest = true;
|
||||
nLastEqualTrustReorg = GetTime();
|
||||
// Prefer earlier timestamp, then lower hash as final tiebreaker
|
||||
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)
|
||||
@@ -3190,16 +3237,16 @@ bool CBlock::AcceptBlock()
|
||||
if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake))
|
||||
return error("AcceptBlock() : AddToBlockIndex failed");
|
||||
|
||||
// Push new tip block directly to all peers. On a small Tor-only
|
||||
// network the inv→getdata→block round-trip adds 1-2 seconds of latency
|
||||
// per hop — enough for a competing staker to create a fork. Pushing
|
||||
// the full block immediately cuts propagation to a single hop.
|
||||
int nBlockEstimate = Checkpoints::GetTotalBlocksEstimate();
|
||||
// Push new tip block directly to peers that are near our tip.
|
||||
// On a small Tor-only network the inv->getdata->block round-trip adds
|
||||
// 1-2 seconds of latency per hop. Pushing immediately cuts propagation
|
||||
// to a single hop. Only push to peers within 10 blocks of our tip —
|
||||
// pushing full blocks to syncing peers wastes bandwidth and slows IBD.
|
||||
if (hashBestChain == hash)
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (nBestHeight > (pnode->nStartingHeight != -1 ? pnode->nStartingHeight - 2000 : nBlockEstimate))
|
||||
if (pnode->nStartingHeight >= nBestHeight - 10)
|
||||
{
|
||||
pnode->PushMessage("block", *this);
|
||||
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
|
||||
@@ -3309,6 +3356,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
}
|
||||
mapOrphanBlocks.insert(make_pair(hash, 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.
|
||||
// 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);
|
||||
pfrom->AddInventoryKnown(inv);
|
||||
|
||||
// Track block delivery for peer latency scoring
|
||||
pfrom->nBlocksDelivered++;
|
||||
|
||||
if (ProcessBlock(pfrom, &block))
|
||||
{
|
||||
mapAlreadyAskedFor.erase(inv);
|
||||
@@ -5328,6 +5379,50 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
||||
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
|
||||
// a while, re-request. Active during IBD (5s timeout) and also
|
||||
|
||||
@@ -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_BLOCKS = 2000;
|
||||
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 int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
||||
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
||||
|
||||
+13
-2
@@ -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());
|
||||
SetThreadPriority(THREAD_PRIORITY_NORMAL);
|
||||
CheckStake(pblock.get(), *pwallet);
|
||||
bool fAccepted = CheckStake(pblock.get(), *pwallet);
|
||||
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
|
||||
MilliSleep(500);
|
||||
|
||||
@@ -273,6 +273,8 @@ public:
|
||||
uint256 hashLastGetHeadersEnd;
|
||||
int nStartingHeight;
|
||||
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
|
||||
std::vector<CAddress> vAddrToSend;
|
||||
@@ -321,6 +323,8 @@ public:
|
||||
hashLastGetHeadersEnd = 0;
|
||||
nStartingHeight = -1;
|
||||
nLastTipCheck = 0;
|
||||
nAvgBlockLatencyUs = 0;
|
||||
nBlocksDelivered = 0;
|
||||
fGetAddr = false;
|
||||
nMisbehavior = 0;
|
||||
hashCheckpointKnown = 0;
|
||||
|
||||
+40
-28
@@ -4,6 +4,7 @@
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -1210,52 +1211,63 @@ uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int
|
||||
class CSignatureCache
|
||||
{
|
||||
private:
|
||||
// sigdata_type is (signature hash, signature, public key):
|
||||
typedef std::tuple<uint256, std::vector<unsigned char>, std::vector<unsigned char> > sigdata_type;
|
||||
std::set< sigdata_type> setValid;
|
||||
// Cache key: hash of (sighash + signature + pubkey) for O(1) lookups.
|
||||
// Using a single uint256 key with unordered_set is much faster than
|
||||
// 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;
|
||||
|
||||
// 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:
|
||||
bool
|
||||
Get(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
|
||||
{
|
||||
LOCK(cs_sigcache);
|
||||
|
||||
sigdata_type k(hash, vchSig, pubKey);
|
||||
std::set<sigdata_type>::iterator mi = setValid.find(k);
|
||||
if (mi != setValid.end())
|
||||
return true;
|
||||
return false;
|
||||
return setValid.count(ComputeKey(hash, vchSig, pubKey)) > 0;
|
||||
}
|
||||
|
||||
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
|
||||
// (~200 bytes per cache entry times 50,000 entries)
|
||||
// Since there are a maximum of 20,000 signature operations per block
|
||||
// 50,000 is a reasonable default.
|
||||
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 50000);
|
||||
// Increased default to 200,000 entries (~1.6MB at 8 bytes each).
|
||||
// The old 50,000 limit was too small and caused frequent evictions.
|
||||
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 200000);
|
||||
if (nMaxCacheSize <= 0) return;
|
||||
|
||||
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
|
||||
// foil would-be DoS attackers who might try to pre-generate
|
||||
// and re-use a set of valid signatures just-slightly-greater
|
||||
// than our cache size.
|
||||
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);
|
||||
auto it = setValid.begin();
|
||||
size_t nTarget = setValid.size() / 2;
|
||||
while (setValid.size() > nTarget && it != setValid.end())
|
||||
it = setValid.erase(it);
|
||||
}
|
||||
|
||||
sigdata_type k(hash, vchSig, pubKey);
|
||||
setValid.insert(k);
|
||||
setValid.insert(ComputeKey(hash, vchSig, pubKey));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+93
-1
@@ -4,6 +4,7 @@
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/version.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -872,26 +873,117 @@ bool CTxDB::GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeigh
|
||||
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 ----------
|
||||
|
||||
bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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))))
|
||||
return true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user