Compare commits

...

1 Commits

Author SHA1 Message Date
sami7777 1c068f4782 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>
2026-04-20 11:55:18 -07:00
10 changed files with 437 additions and 128 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 invgetdatablock 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
+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_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
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());
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);
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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;