Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22e220acaa | |||
| 1c068f4782 | |||
| a671708f0b | |||
| be90d39cd4 | |||
| 4d0478add5 | |||
| 734979c93b | |||
| 00af636aca | |||
| 9377b3a52f | |||
| 1881ff867e | |||
| caddfb1789 |
+1
-1
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
project(Triangles
|
||||
VERSION 5.8.1.0
|
||||
VERSION 5.8.6
|
||||
DESCRIPTION "Cryptographic Triangles Wallet"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Triangles Modernization Roadmap
|
||||
|
||||
**Goal:** Make TRI faster to sync, safer for wallets, and more useful as a currency — without breaking consensus.
|
||||
|
||||
**Invariant:** Any change that modifies block validation, stake modifier computation, transaction format, or signature verification MUST preserve exact consensus with existing v5.x nodes. When in doubt, test against a synced v5.8.1 node.
|
||||
|
||||
---
|
||||
|
||||
## Priority 1: Faster Syncing (High Impact, Low Risk)
|
||||
|
||||
### 1.1 Update Checkpoints (Easy, Immediate)
|
||||
**Problem:** Last hardcoded checkpoint is at block 2,186,940. `IsInitialBlockDownload()` returns false past this point, causing orphan limit to drop from 4000 to 750 — exactly what caused the fork deadlock.
|
||||
|
||||
**Fix:** Add checkpoints every ~50,000 blocks up to current height (~2,207,000+).
|
||||
```cpp
|
||||
// src/checkpoints.cpp - add recent checkpoints
|
||||
{2190000, uint256("...")},
|
||||
{2195000, uint256("...")},
|
||||
{2200000, uint256("...")},
|
||||
{2205000, uint256("...")},
|
||||
{2210000, uint256("...")},
|
||||
```
|
||||
**Risk:** None — checkpoints are only used for IBD detection and quick rejection of clearly wrong chains.
|
||||
|
||||
### 1.2 Increase Post-Checkpoint Orphan Limit (Easy)
|
||||
**Problem:** 750 orphans after IBD is too low for a low-peer network. During the fork incident, 750 orphans filled up and the node deadlocked.
|
||||
|
||||
**Fix:**
|
||||
```cpp
|
||||
// src/main.h
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS = 2000; // was 750
|
||||
```
|
||||
**Risk:** Slightly more memory usage during forks. Worth it for resilience.
|
||||
|
||||
### 1.3 Parallel Block Download (Medium Effort)
|
||||
**Problem:** Current implementation downloads blocks sequentially from one peer at a time during IBD.
|
||||
|
||||
**Fix:** Increase batch sizes and allow concurrent block downloads from multiple peers:
|
||||
```cpp
|
||||
// src/main.cpp
|
||||
// During IBD, request blocks from multiple peers simultaneously
|
||||
unsigned int nGetDataBatchSize = IsInitialBlockDownload() ? 8000 : 1000; // was 4000
|
||||
```
|
||||
**Risk:** Low — larger batch sizes are already proven in Bitcoin forks.
|
||||
|
||||
### 1.4 Header-First Sync (Medium Effort)
|
||||
**Problem:** Node downloads full blocks before validating headers. A bad peer can waste bandwidth.
|
||||
|
||||
**Fix:** Download and validate all headers first (compact ~80 bytes each), then download full blocks only for the best chain.
|
||||
- Separate `getheaders`/`headers` message handling
|
||||
- Download blocks only for the best header chain
|
||||
- Reduces wasted bandwidth during forks by 95%+
|
||||
|
||||
### 1.5 Bootstrap Over HTTPS with Resume (Easy)
|
||||
**Problem:** Built-in bootstrap (`-bootstrap`) uses raw TCP and can't resume interrupted downloads.
|
||||
|
||||
**Fix:** The existing `bootstrap.cpp` already supports downloading. Add:
|
||||
- Resume support (Range headers)
|
||||
- SHA256 verification of downloaded archive
|
||||
- Better progress reporting
|
||||
- Fallback mirrors
|
||||
|
||||
---
|
||||
|
||||
## Priority 2: Wallet Safety (Critical)
|
||||
|
||||
### 2.1 Automatic Wallet Backup Before Dangerous Operations (Easy)
|
||||
**Problem:** Corrupt wallet = lost funds. No automatic backup before risky operations.
|
||||
|
||||
**Fix:** In `walletdb.cpp`, before any rewrite:
|
||||
```cpp
|
||||
// Before wallet.dat rewrite, copy to wallet.dat.bak
|
||||
if (boost::filesystem::exists(pathWallet)) {
|
||||
boost::filesystem::copy_file(pathWallet, pathWallet + ".bak",
|
||||
boost::filesystem::copy_option::overwrite_if_exists);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Detect and Report BDB Corruption (Easy)
|
||||
**Problem:** BDB corruption silently corrupts wallet. User doesn't know until it's too late.
|
||||
|
||||
**Fix:** Add wallet integrity check on load:
|
||||
```cpp
|
||||
// In CWallet::LoadWallet()
|
||||
// After opening, verify BDB environment is healthy
|
||||
// If DB_RUNRECOVERY, auto-salvage and warn user
|
||||
```
|
||||
|
||||
### 2.3 Wallet.dat Versioning (Medium Effort)
|
||||
**Problem:** Single wallet.dat file. If it corrupts during write, funds are lost.
|
||||
|
||||
**Fix:** Implement copy-on-write wallet saves:
|
||||
- Write new wallet data to `wallet.dat.new`
|
||||
- Atomically rename `wallet.dat` → `wallet.dat.old`, `wallet.dat.new` → `wallet.dat`
|
||||
- Keep last 3 wallet revisions
|
||||
- On load, try wallet.dat first, fall back to wallet.dat.old if corrupt
|
||||
|
||||
### 2.4 Seed Phrase / HD Wallet (High Effort, High Impact)
|
||||
**Problem:** Losing wallet.dat = losing everything. No recovery mechanism.
|
||||
|
||||
**Fix:** Implement BIP39/BIP44 HD wallet as optional upgrade:
|
||||
- Generate 12/24-word seed phrase on new wallet creation
|
||||
- Derive all keys from seed deterministically
|
||||
- Import seed on any device to recover wallet
|
||||
- Keep backward compatibility with existing non-HD wallets
|
||||
|
||||
---
|
||||
|
||||
## Priority 3: Network Resilience (Medium Impact)
|
||||
|
||||
### 3.1 Better Peer Management (Medium Effort)
|
||||
**Problem:** Low peer counts (2-6) lead to fork divergence. No prioritization of reliable peers.
|
||||
|
||||
**Fix:**
|
||||
- Peer reliability scoring (track which peers provide valid blocks)
|
||||
- Prefer peers that are ahead and on the same chain
|
||||
- Automatic disconnection of stale/forked peers
|
||||
- Increase default `maxconnections` from 64 to 128
|
||||
|
||||
### 3.2 Compact Block Relay (High Effort)
|
||||
**Problem:** Full blocks are sent even when the receiver likely already has most transactions.
|
||||
|
||||
**Fix:** Implement BIP 152 compact blocks:
|
||||
- Send block header + short transaction IDs
|
||||
- Receiver fills in from mempool, only requests missing transactions
|
||||
- Reduces bandwidth by ~90% during normal operation
|
||||
|
||||
### 3.3 DNS Seed Infrastructure (Easy)
|
||||
**Problem:** `dnsseed=0` when Tor-only means no automatic peer discovery.
|
||||
|
||||
**Fix:** Run a DNS seed server that resolves to known reliable onion addresses:
|
||||
```
|
||||
seed.cryptographic-triangles.org → returns onion addresses of healthy nodes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority 4: User Experience (Medium Impact)
|
||||
|
||||
### 4.1 Progress Reporting for IBD (Easy)
|
||||
**Problem:** Users see "downloading blocks..." with no useful progress indicator.
|
||||
|
||||
**Fix:**
|
||||
- Report `headers` vs `blocks` progress separately
|
||||
- Show estimated time remaining based on download speed
|
||||
- Log progress every 1000 blocks (currently every 5000)
|
||||
- Qt wallet: update progress bar more frequently
|
||||
|
||||
### 4.2 Staking Dashboard Improvements (Easy)
|
||||
**Problem:** Qt wallet shows staking info but not clearly.
|
||||
|
||||
**Fix:**
|
||||
- Show expected time to stake more prominently
|
||||
- Display staking weight as percentage of network
|
||||
- Notify when stake is found (system notification)
|
||||
- Show "staking" indicator in system tray
|
||||
|
||||
### 4.3 Transaction Fee Estimation (Medium Effort)
|
||||
**Problem:** No fee estimation. Users guess.
|
||||
|
||||
**Fix:** Track recent block inclusion rates by fee level, provide fee recommendations.
|
||||
|
||||
---
|
||||
|
||||
## Priority 5: Code Modernization (Low Urgency, Good Hygiene)
|
||||
|
||||
### 5.1 C++17/20 Features
|
||||
- Replace raw pointers with smart pointers where safe
|
||||
- Use `std::optional`, `std::string_view`, `std::filesystem`
|
||||
- Replace boost::filesystem with std::filesystem (C++17)
|
||||
|
||||
### 5.2 Build System
|
||||
- CMake is already in place (good)
|
||||
- Add sanitizers (ASAN, UBSAN) to CI
|
||||
- Static analysis with clang-tidy
|
||||
|
||||
### 5.3 Testing
|
||||
- Current test coverage is thin
|
||||
- Add unit tests for:
|
||||
- Checkpoint validation
|
||||
- Stake modifier computation
|
||||
- Bootstrap download/resume
|
||||
- Wallet BDB recovery
|
||||
- Orphan block handling
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Change
|
||||
|
||||
These are consensus-critical and must remain identical:
|
||||
- Block validation rules
|
||||
- Stake modifier computation (`ComputeNextStakeModifier`)
|
||||
- Transaction signature verification
|
||||
- Block reward schedule
|
||||
- PoW/PoS target computation
|
||||
- Chain trust / difficulty adjustment
|
||||
- Message serialization format
|
||||
- Protocol version handshaking
|
||||
|
||||
Any change to these requires a coordinated network upgrade (hard fork).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **This week:** Update checkpoints (1.1), increase orphan limit (1.2), wallet backup before save (2.1)
|
||||
2. **Next week:** Better progress reporting (4.1), increase batch size (1.3)
|
||||
3. **Month 1:** Wallet versioning (2.3), bootstrap resume (1.5)
|
||||
4. **Month 2:** Header-first sync (1.4), peer reliability (3.1)
|
||||
5. **Month 3+:** HD wallet (2.4), compact blocks (3.2)
|
||||
@@ -27,12 +27,14 @@ Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion:24112`
|
||||
- `futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion:24112`
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**DNS Seeds:**
|
||||
- `seed1.cryptographic-triangles.org`
|
||||
- `seed2.cryptographic-triangles.org`
|
||||
**HTTP Seed List:**
|
||||
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
||||
|
||||
## Building from Source
|
||||
|
||||
|
||||
+281
-68
@@ -14,6 +14,9 @@
|
||||
#include "netbase.h"
|
||||
#include "net.h"
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
@@ -22,8 +25,10 @@
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -40,80 +45,287 @@ 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)
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
struct addrinfo hints, *result, *rp;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
std::string portStr = std::to_string(port);
|
||||
int rc = getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result);
|
||||
if (rc != 0) {
|
||||
strError = "DNS resolution failed for " + host;
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
return true;
|
||||
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
||||
hSocket = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
continue;
|
||||
|
||||
if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
|
||||
break; // success
|
||||
|
||||
closesocket(hSocket);
|
||||
hSocket = INVALID_SOCKET;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
strError = "Cannot connect to " + host + ":" + portStr;
|
||||
|
||||
return hSocket;
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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,
|
||||
std::string& strError)
|
||||
std::string& strError,
|
||||
bool noProxy)
|
||||
{
|
||||
try {
|
||||
// Connect through Tor SOCKS proxy (ConnectSocketByName respects SetProxy)
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
CService addr;
|
||||
if (!ConnectSocketByName(addr, hSocket, host.c_str(), PORT, 30)) {
|
||||
strError = "Cannot connect to " + host + " (check Tor proxy)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send HTTP GET request
|
||||
std::string request =
|
||||
"GET " + urlPath + " HTTP/1.1\r\n"
|
||||
"Host: " + host + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Triangles\r\n"
|
||||
"\r\n";
|
||||
|
||||
if (!SendAll(hSocket, request.data(), request.size())) {
|
||||
closesocket(hSocket);
|
||||
strError = "Failed to send request to " + host;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read response headers
|
||||
std::string currentHost = host;
|
||||
std::string currentPath = urlPath;
|
||||
int currentPort = PORT;
|
||||
bool useSSL = false;
|
||||
std::string headerData;
|
||||
if (!RecvUntil(hSocket, headerData, "\r\n\r\n")) {
|
||||
closesocket(hSocket);
|
||||
strError = "Failed to read HTTP headers from " + host;
|
||||
return false;
|
||||
}
|
||||
int redirectCount = 0;
|
||||
const int MAX_REDIRECTS = 5;
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN ..."
|
||||
unsigned int status_code = 0;
|
||||
size_t sp = headerData.find(' ');
|
||||
if (sp != std::string::npos)
|
||||
status_code = atoi(headerData.c_str() + sp + 1);
|
||||
HttpConn conn;
|
||||
|
||||
if (status_code != 200) {
|
||||
closesocket(hSocket);
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + urlPath;
|
||||
return false;
|
||||
// Connection + redirect loop
|
||||
while (true) {
|
||||
conn.Close(); // clean slate for each attempt
|
||||
|
||||
if (noProxy) {
|
||||
conn.sock = ConnectDirectTCP(currentHost, currentPort, strError);
|
||||
if (conn.sock == INVALID_SOCKET)
|
||||
return false;
|
||||
} else {
|
||||
CService addr;
|
||||
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"
|
||||
"Host: " + currentHost + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Triangles\r\n"
|
||||
"\r\n";
|
||||
|
||||
if (!conn.Send(request.data(), request.size())) {
|
||||
strError = "Failed to send request to " + currentHost;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read response headers
|
||||
if (!conn.RecvUntil(headerData, "\r\n\r\n")) {
|
||||
strError = "Failed to read HTTP headers from " + currentHost;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN ..."
|
||||
unsigned int status_code = 0;
|
||||
size_t sp = headerData.find(' ');
|
||||
if (sp != std::string::npos)
|
||||
status_code = atoi(headerData.c_str() + sp + 1);
|
||||
|
||||
// Handle HTTP redirects
|
||||
if (status_code == 301 || status_code == 302 ||
|
||||
status_code == 307 || status_code == 308) {
|
||||
|
||||
if (++redirectCount > MAX_REDIRECTS) {
|
||||
strError = "Too many redirects for " + urlPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find Location header (case-insensitive)
|
||||
std::string lowerHdr = headerData;
|
||||
std::transform(lowerHdr.begin(), lowerHdr.end(),
|
||||
lowerHdr.begin(), ::tolower);
|
||||
size_t locPos = lowerHdr.find("\nlocation:");
|
||||
if (locPos == std::string::npos) {
|
||||
strError = "Redirect " + std::to_string(status_code) + " without Location header";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t valStart = locPos + 10; // skip "\nlocation:"
|
||||
while (valStart < headerData.size() && headerData[valStart] == ' ')
|
||||
valStart++;
|
||||
size_t lineEnd = headerData.find("\r\n", valStart);
|
||||
std::string location;
|
||||
if (lineEnd != std::string::npos)
|
||||
location = headerData.substr(valStart, lineEnd - valStart);
|
||||
else
|
||||
location = headerData.substr(valStart);
|
||||
boost::trim(location);
|
||||
|
||||
// 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;
|
||||
}
|
||||
} else if (!location.empty() && location[0] == '/') {
|
||||
currentPath = location;
|
||||
} else {
|
||||
strError = "Unsupported redirect location: " + location;
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
break; // Got 200, proceed to download
|
||||
}
|
||||
|
||||
// Parse Content-Length
|
||||
@@ -132,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;
|
||||
}
|
||||
@@ -143,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;
|
||||
@@ -163,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) {
|
||||
@@ -183,13 +393,14 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
|
||||
bool FetchFileList(const std::string& host,
|
||||
std::vector<std::string>& files,
|
||||
std::string& strError)
|
||||
std::string& strError,
|
||||
bool noProxy)
|
||||
{
|
||||
// Download filelist.txt to a temp file
|
||||
fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt";
|
||||
|
||||
std::string urlPath = std::string(BASE_PATH) + "filelist.txt";
|
||||
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError))
|
||||
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy))
|
||||
return false;
|
||||
|
||||
// Read lines
|
||||
@@ -458,10 +669,12 @@ bool DownloadBootstrap(const std::string& host,
|
||||
bool gotBlockFile = false;
|
||||
|
||||
// Try downloading bootstrap.tar.gz first
|
||||
// Bootstrap server is on clearnet — bypass Tor proxy for DNS + HTTP
|
||||
const bool noProxy = true;
|
||||
fs::path tmpTarGz = dataDir / "bootstrap.tar.gz.tmp";
|
||||
std::string tarUrl = std::string(BASE_PATH) + "triangles-bootstrap.tar.gz";
|
||||
|
||||
bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError);
|
||||
bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError, noProxy);
|
||||
|
||||
if (tarDownloaded) {
|
||||
bool extractOk = ExtractTarGz(tmpTarGz, dataDir, strError);
|
||||
@@ -476,7 +689,7 @@ bool DownloadBootstrap(const std::string& host,
|
||||
// Fallback: try filelist.txt + individual file downloads
|
||||
std::string fallbackError;
|
||||
std::vector<std::string> files;
|
||||
if (!FetchFileList(host, files, fallbackError)) {
|
||||
if (!FetchFileList(host, files, fallbackError, noProxy)) {
|
||||
if (!tarDownloaded)
|
||||
strError = strError + " (fallback also failed: " + fallbackError + ")";
|
||||
else
|
||||
@@ -489,7 +702,7 @@ bool DownloadBootstrap(const std::string& host,
|
||||
fs::create_directories(destPath.parent_path());
|
||||
|
||||
std::string urlPath = std::string(BASE_PATH) + files[i];
|
||||
if (!DownloadFile(host, urlPath, destPath, progressFn, strError))
|
||||
if (!DownloadFile(host, urlPath, destPath, progressFn, strError, noProxy))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -22,16 +22,20 @@ namespace Bootstrap {
|
||||
// Check if data dir already has blockchain data
|
||||
bool NeedsBootstrap(const boost::filesystem::path& dataDir);
|
||||
|
||||
// Download a single file via HTTP GET, write to destPath
|
||||
// Download a single file via HTTP GET, write to destPath.
|
||||
// If noProxy is true, bypass Tor SOCKS proxy and connect directly
|
||||
// (used for clearnet bootstrap downloads).
|
||||
bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
const boost::filesystem::path& destPath,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError);
|
||||
std::string& strError,
|
||||
bool noProxy = false);
|
||||
|
||||
// Fetch the file manifest (list of relative paths to download)
|
||||
bool FetchFileList(const std::string& host,
|
||||
std::vector<std::string>& files,
|
||||
std::string& strError);
|
||||
std::string& strError,
|
||||
bool noProxy = false);
|
||||
|
||||
// Download bootstrap.tar.gz and extract to dataDir.
|
||||
// Falls back to filelist.txt + individual file download if tar.gz unavailable.
|
||||
|
||||
@@ -33,6 +33,13 @@ namespace Checkpoints
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
|
||||
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
|
||||
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
|
||||
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
|
||||
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
|
||||
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
|
||||
};
|
||||
|
||||
static MapCheckpoints mapCheckpointsTestnet = {
|
||||
@@ -49,6 +56,13 @@ namespace Checkpoints
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
|
||||
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
|
||||
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
|
||||
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
|
||||
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
|
||||
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
|
||||
};
|
||||
|
||||
bool CheckHardened(int nHeight, const uint256& hash)
|
||||
|
||||
+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 1
|
||||
#define CLIENT_VERSION_REVISION 6
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
@@ -28,6 +28,7 @@ extern unsigned int nWalletDBUpdated;
|
||||
|
||||
void ThreadFlushWalletDB(void* parg);
|
||||
bool BackupWallet(const CWallet& wallet, const std::string& strDest);
|
||||
bool AutoBackupWallet(const boost::filesystem::path& walletPath);
|
||||
|
||||
|
||||
class CDBEnv
|
||||
|
||||
@@ -952,6 +952,18 @@ bool AppInit2()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle -reindex: delete the LevelDB block index so it gets rebuilt
|
||||
// from the raw blk*.dat files via FastImportBlockFile().
|
||||
// This recalculates money supply, tx index, and UTXO set from scratch.
|
||||
if (GetBoolArg("-reindex", false))
|
||||
{
|
||||
printf("Reindex requested: removing block index database...\n");
|
||||
uiInterface.InitMessage(_("Removing block index for reindex..."));
|
||||
fs::path txleveldbPath = GetDataDir() / "txleveldb";
|
||||
if (fs::exists(txleveldbPath))
|
||||
fs::remove_all(txleveldbPath);
|
||||
}
|
||||
|
||||
uiInterface.InitMessage(_("Loading block index..."));
|
||||
printf("Loading block index...\n");
|
||||
nStart = GetTimeMillis();
|
||||
@@ -1041,6 +1053,21 @@ bool AppInit2()
|
||||
nStart = GetTimeMillis();
|
||||
bool fFirstRun = true;
|
||||
pwalletMain = new CWallet(strWalletFileName);
|
||||
|
||||
// Auto-backup wallet.dat before loading (protects against corruption during load/flush)
|
||||
{
|
||||
fs::path walletPath = GetDataDir() / strWalletFileName;
|
||||
if (fs::exists(walletPath)) {
|
||||
uintmax_t wsize = fs::file_size(walletPath);
|
||||
printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize);
|
||||
if (wsize < 1024) {
|
||||
strErrors << _("WARNING: wallet.dat is suspiciously small (") << wsize << _(" bytes). It may be corrupt.\n");
|
||||
printf("WARNING: wallet.dat is only %llu bytes - possibly corrupt!\n", (unsigned long long)wsize);
|
||||
}
|
||||
AutoBackupWallet(walletPath);
|
||||
}
|
||||
}
|
||||
|
||||
DBErrors nLoadWalletRet = pwalletMain->LoadWallet(fFirstRun);
|
||||
if (nLoadWalletRet != DB_LOAD_OK)
|
||||
{
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+396
-56
@@ -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>
|
||||
@@ -70,6 +71,7 @@ uint256 nBestInvalidTrust = 0;
|
||||
|
||||
uint256 hashBestChain = 0;
|
||||
CBlockIndex* pindexBest = NULL;
|
||||
CBlockIndex* pindexFinalized = NULL; // auto-checkpoint: deepest finalized block
|
||||
bool fAddressIndex = false;
|
||||
int64_t nTimeBestReceived = 0;
|
||||
|
||||
@@ -116,8 +118,10 @@ static CCriticalSection cs_PostIbdWork;
|
||||
static bool fPostIbdWorkStarted = false;
|
||||
|
||||
static const unsigned int MAX_HEADER_SYNC_CACHE = 50000;
|
||||
static const unsigned int HEADER_DOWNLOAD_WINDOW = 128;
|
||||
static const unsigned int HEADER_DOWNLOAD_WINDOW = 512; // Increased from 128 for parallel downloads
|
||||
static const unsigned int HEADER_DOWNLOAD_PER_PEER = 64; // Max blocks to request from each peer
|
||||
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 30 * 1000000;
|
||||
static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 10 * 1000000; // Request from another peer after 10s
|
||||
|
||||
static void ThreadPostIbdWork(void* parg)
|
||||
{
|
||||
@@ -405,6 +409,106 @@ static void ContinueHeaderSync(CNode* pfrom, const uint256& hashTip)
|
||||
pfrom->PushMessage("getheaders", locator, uint256(0));
|
||||
}
|
||||
|
||||
// Parallel block downloading: distribute blocks across all available peers
|
||||
static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
|
||||
{
|
||||
if (hashBestHeaderSync == 0)
|
||||
return 0;
|
||||
|
||||
const std::vector<uint256> vPath = GetHeaderSyncDownloadPath(hashBestHeaderSync);
|
||||
if (vPath.empty())
|
||||
return 0;
|
||||
|
||||
// Collect eligible peers
|
||||
std::vector<CNode*> vEligiblePeers;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
{
|
||||
if (!pnode->fClient && pnode->nVersion != 0 && !pnode->fDisconnect)
|
||||
vEligiblePeers.push_back(pnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (vEligiblePeers.empty())
|
||||
return 0;
|
||||
|
||||
const int64_t nNow = GetTime() * 1000000;
|
||||
unsigned int nInFlight = CountHeaderSyncInFlight();
|
||||
unsigned int nQueued = 0;
|
||||
unsigned int nPeerIndex = 0;
|
||||
|
||||
// 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)
|
||||
break;
|
||||
|
||||
std::map<uint256, CHeaderSyncNode>::iterator mi = mapHeaderSync.find(*it);
|
||||
if (mi == mapHeaderSync.end())
|
||||
continue;
|
||||
|
||||
// Check if already requested recently
|
||||
bool fNeedsRequest = false;
|
||||
if (!mi->second.fRequested)
|
||||
{
|
||||
// Never requested - request now
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
else if (nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
|
||||
{
|
||||
// Timeout expired - retry
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
else if (nNow - mi->second.nLastRequestTime >= HEADER_REDUNDANT_REQUEST_MICROS)
|
||||
{
|
||||
// Redundant request: ask another peer if original is slow
|
||||
// This creates parallel downloads for slow blocks
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
|
||||
if (!fNeedsRequest)
|
||||
continue;
|
||||
|
||||
// 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)
|
||||
if (!mi->second.fRequested || nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
|
||||
{
|
||||
mi->second.fRequested = true;
|
||||
mi->second.nLastRequestTime = nNow;
|
||||
}
|
||||
|
||||
++nQueued;
|
||||
++nPeerIndex;
|
||||
}
|
||||
|
||||
if (nQueued > 0)
|
||||
printf("IBD-DIAG: parallel queue distributed %u blocks across %zu peers (window=%u, inflight=%u)\n",
|
||||
nQueued, vEligiblePeers.size(), nWindow, nInFlight);
|
||||
|
||||
return nQueued;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -1322,6 +1426,57 @@ uint256 WantedByOrphan(const CBlock* pblockOrphan)
|
||||
return pblockOrphan->hashPrevBlock;
|
||||
}
|
||||
|
||||
// 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 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())
|
||||
continue;
|
||||
|
||||
CBlock* pblockEvict = it->second;
|
||||
|
||||
// Remove from by-prev index
|
||||
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
||||
range.first != range.second; ++range.first)
|
||||
{
|
||||
if (range.first->second == pblockEvict) {
|
||||
mapOrphanBlocksByPrev.erase(range.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setStakeSeenOrphan.erase(pblockEvict->GetProofOfStake());
|
||||
delete pblockEvict;
|
||||
mapOrphanBlocks.erase(evictHash);
|
||||
nEvicted++;
|
||||
}
|
||||
|
||||
if (nEvicted > 0)
|
||||
printf("LimitOrphanBlocks: evicted %u oldest orphan(s), %u remain\n",
|
||||
nEvicted, (unsigned int)mapOrphanBlocks.size());
|
||||
|
||||
return nEvicted;
|
||||
}
|
||||
|
||||
// miner's coin base reward
|
||||
int64_t GetProofOfWorkReward(int64_t nFees)
|
||||
{
|
||||
@@ -1415,9 +1570,13 @@ static unsigned int GetNextTargetRequired_(const CBlockIndex* pindexLast, bool f
|
||||
return bnTargetLimit.GetCompact(); // genesis block
|
||||
|
||||
const CBlockIndex* pindexPrev = GetLastBlockIndex(pindexLast, fProofOfStake);
|
||||
if (pindexPrev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // no previous block of this type
|
||||
if (pindexPrev->pprev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // first block
|
||||
const CBlockIndex* pindexPrevPrev = GetLastBlockIndex(pindexPrev->pprev, fProofOfStake);
|
||||
if (pindexPrevPrev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // no second previous block of this type
|
||||
if (pindexPrevPrev->pprev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // second block
|
||||
|
||||
@@ -2324,7 +2483,15 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
|
||||
bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
{
|
||||
printf("REORGANIZE\n");
|
||||
printf("REORGANIZE: Switching chains\n");
|
||||
printf(" Old tip: %s height %d trust %s\n",
|
||||
pindexBest->GetBlockHash().ToString().substr(0,20).c_str(),
|
||||
pindexBest->nHeight,
|
||||
CBigNum(pindexBest->nChainTrust).ToString().c_str());
|
||||
printf(" New tip: %s height %d trust %s\n",
|
||||
pindexNew->GetBlockHash().ToString().substr(0,20).c_str(),
|
||||
pindexNew->nHeight,
|
||||
CBigNum(pindexNew->nChainTrust).ToString().c_str());
|
||||
|
||||
// Find the fork
|
||||
CBlockIndex* pfork = pindexBest;
|
||||
@@ -2340,6 +2507,27 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
return error("Reorganize() : pfork->pprev is null");
|
||||
}
|
||||
|
||||
// Finality: reject reorgs that go below the auto-checkpoint or
|
||||
// exceed MAX_REORG_DEPTH blocks. During IBD we allow deep reorgs
|
||||
// since we haven't settled on a tip yet.
|
||||
if (!IsInitialBlockDownload())
|
||||
{
|
||||
if (pindexFinalized && pfork->nHeight < pindexFinalized->nHeight)
|
||||
{
|
||||
printf("REORGANIZE: REJECTED — fork at %d is below finalized block %d\n",
|
||||
pfork->nHeight, pindexFinalized->nHeight);
|
||||
return error("Reorganize() : fork point %d below auto-checkpoint %d",
|
||||
pfork->nHeight, pindexFinalized->nHeight);
|
||||
}
|
||||
unsigned int nDisconnectDepth = pindexBest->nHeight - pfork->nHeight;
|
||||
if (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)
|
||||
@@ -2351,8 +2539,17 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
vConnect.push_back(pindex);
|
||||
reverse(vConnect.begin(), vConnect.end());
|
||||
|
||||
printf("REORGANIZE: Disconnect %" PRIszu " blocks; %s..%s\n", vDisconnect.size(), pfork->GetBlockHash().ToString().substr(0,20).c_str(), pindexBest->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Connect %" PRIszu " blocks; %s..%s\n", vConnect.size(), pfork->GetBlockHash().ToString().substr(0,20).c_str(), pindexNew->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Fork point at height %d: %s\n",
|
||||
pfork->nHeight,
|
||||
pfork->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Disconnect %" PRIszu " blocks (heights %d..%d)\n",
|
||||
vDisconnect.size(),
|
||||
pfork->nHeight + 1,
|
||||
pindexBest->nHeight);
|
||||
printf("REORGANIZE: Connect %" PRIszu " blocks (heights %d..%d)\n",
|
||||
vConnect.size(),
|
||||
pfork->nHeight + 1,
|
||||
pindexNew->nHeight);
|
||||
|
||||
// Disconnect shorter branch
|
||||
vector<CTransaction> vResurrect;
|
||||
@@ -2370,12 +2567,6 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
vResurrect.push_back(tx);
|
||||
}
|
||||
|
||||
// Remove disconnected PoS blocks from setStakeSeen so they don't
|
||||
// block acceptance of valid blocks on the winning chain.
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->IsProofOfStake())
|
||||
setStakeSeen.erase(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
|
||||
// Connect longer branch
|
||||
vector<CTransaction> vDelete;
|
||||
for (unsigned int i = 0; i < vConnect.size(); i++)
|
||||
@@ -2403,19 +2594,37 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
if (!txdb.TxnCommit())
|
||||
return error("Reorganize() : TxnCommit failed");
|
||||
|
||||
// Disconnect shorter branch
|
||||
// ======================================================================
|
||||
// CRITICAL: All operations below this point must be in-memory only and
|
||||
// should never fail. The DB transaction is committed, so we cannot abort.
|
||||
// ======================================================================
|
||||
|
||||
// Disconnect shorter branch (in-memory only)
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->pprev)
|
||||
pindex->pprev->pnext = NULL;
|
||||
|
||||
// Connect longer branch
|
||||
// Connect longer branch (in-memory only)
|
||||
for (CBlockIndex* pindex : vConnect)
|
||||
if (pindex->pprev)
|
||||
pindex->pprev->pnext = pindex;
|
||||
|
||||
// Remove disconnected PoS blocks from setStakeSeen so they don't
|
||||
// block acceptance of valid blocks on the winning chain.
|
||||
// This MUST happen after commit to maintain consistency.
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->IsProofOfStake())
|
||||
setStakeSeen.erase(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
|
||||
// Resurrect memory transactions that were in the disconnected branch
|
||||
unsigned int nResurrected = 0;
|
||||
for (CTransaction& tx : vResurrect)
|
||||
tx.AcceptToMemoryPool(txdb, false);
|
||||
{
|
||||
if (tx.AcceptToMemoryPool(txdb, false))
|
||||
nResurrected++;
|
||||
}
|
||||
if (nResurrected > 0)
|
||||
printf("REORGANIZE: resurrected %u transactions to mempool\n", nResurrected);
|
||||
|
||||
// Delete redundant memory transactions that are in the connected branch
|
||||
for (CTransaction& tx : vDelete) {
|
||||
@@ -2423,7 +2632,8 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
mempool.removeConflicts(tx);
|
||||
}
|
||||
|
||||
printf("REORGANIZE: done\n");
|
||||
printf("REORGANIZE: done (fork at height %d, %zu disconnected, %zu connected)\n",
|
||||
pfork->nHeight, vDisconnect.size(), vConnect.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2540,6 +2750,23 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
nTimeBestReceived = GetTime();
|
||||
nTransactionsUpdated++;
|
||||
|
||||
// Auto-checkpoint: finalize the block at depth MAX_REORG_DEPTH.
|
||||
// Only set when fully synced (not IBD) so we don't lock in a
|
||||
// potentially wrong chain during initial sync.
|
||||
if (!IsInitialBlockDownload() && nBestHeight > (int)MAX_REORG_DEPTH)
|
||||
{
|
||||
CBlockIndex* pcandidate = pindexBest;
|
||||
for (int i = 0; i < (int)MAX_REORG_DEPTH && pcandidate; i++)
|
||||
pcandidate = pcandidate->pprev;
|
||||
if (pcandidate && pcandidate != pindexFinalized)
|
||||
{
|
||||
pindexFinalized = pcandidate;
|
||||
printf("AUTO-CHECKPOINT: block %d (%s) is now finalized\n",
|
||||
pindexFinalized->nHeight,
|
||||
pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
uint256 nBestBlockTrust = (pindexBest->nHeight != 0 && pindexBest->pprev) ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
||||
|
||||
// Log every 5000 blocks during sync, every block once caught up
|
||||
@@ -2613,6 +2840,9 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
{
|
||||
printf("*** Initial block download complete at height %d ***\n", nBestHeight);
|
||||
|
||||
// Trim orphan blocks to normal limit now that IBD is done
|
||||
LimitOrphanBlocks(MAX_ORPHAN_BLOCKS);
|
||||
|
||||
// Update wallet best chain locator now that IBD is done
|
||||
const CBlockLocator locator(pindexBest);
|
||||
::SetBestChain(locator);
|
||||
@@ -2802,22 +3032,61 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
|
||||
// writes to the same transaction, cutting the per-block commit count in half.
|
||||
//
|
||||
// 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 10 minutes to prevent
|
||||
// oscillation from Tor-latency-induced competing announcements.
|
||||
// 1. Linear extension: always accept (no reorg needed).
|
||||
// 2. Side-chain reorg: require 10% more cumulative trust than current
|
||||
// best chain. This gives a strong "first-seen" advantage and
|
||||
// prevents endless fork-thrashing on a small network.
|
||||
// During IBD the delta is waived so the heaviest chain wins.
|
||||
// 3. 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 > 10 * 60)
|
||||
{
|
||||
fNewBest = true;
|
||||
nLastEqualTrustReorg = GetTime();
|
||||
bool fLinearExtension = (pindexNew->pprev == pindexBest);
|
||||
if (fLinearExtension || IsInitialBlockDownload())
|
||||
{
|
||||
fNewBest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Side-chain reorg: require 10% more trust.
|
||||
// new * 10 > best * 11 ⟺ new > best * 1.1
|
||||
CBigNum bnNewTrust(pindexNew->nChainTrust);
|
||||
CBigNum bnBestTrust(nBestChainTrust);
|
||||
if (bnNewTrust * 10 > bnBestTrust * 11)
|
||||
{
|
||||
fNewBest = true;
|
||||
printf("CHAIN: Side-chain reorg accepted (trust delta sufficient)\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("CHAIN: Side-chain at height %d REJECTED — insufficient trust delta "
|
||||
"(need >10%% more, have %s vs %s)\n",
|
||||
pindexNew->nHeight,
|
||||
bnNewTrust.ToString().c_str(),
|
||||
bnBestTrust.ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
|
||||
GetTime() - nLastEqualTrustReorg > 2 * 60)
|
||||
{
|
||||
// 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)
|
||||
@@ -3026,14 +3295,20 @@ bool CBlock::AcceptBlock()
|
||||
if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake))
|
||||
return error("AcceptBlock() : AddToBlockIndex failed");
|
||||
|
||||
// Relay inventory, but don't relay old inventory during initial block download
|
||||
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))
|
||||
pnode->PushInventory(CInv(MSG_BLOCK, hash));
|
||||
if (pnode->nStartingHeight >= nBestHeight - 10)
|
||||
{
|
||||
pnode->PushMessage("block", *this);
|
||||
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -3139,34 +3414,13 @@ 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
|
||||
// downloads don't get evicted and re-requested.
|
||||
unsigned int nMaxOrphans = IsInitialBlockDownload() ? MAX_ORPHAN_BLOCKS_IBD : MAX_ORPHAN_BLOCKS;
|
||||
if (mapOrphanBlocks.size() > nMaxOrphans)
|
||||
{
|
||||
// Evict a random orphan
|
||||
uint256 randomhash = GetRandHash();
|
||||
auto it = mapOrphanBlocks.lower_bound(randomhash);
|
||||
if (it == mapOrphanBlocks.end())
|
||||
it = mapOrphanBlocks.begin();
|
||||
CBlock* pblockEvict = it->second;
|
||||
uint256 evictHash = it->first;
|
||||
// Remove from by-prev index
|
||||
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
||||
range.first != range.second; ++range.first)
|
||||
{
|
||||
if (range.first->second == pblockEvict) {
|
||||
mapOrphanBlocksByPrev.erase(range.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setStakeSeenOrphan.erase(pblockEvict->GetProofOfStake());
|
||||
delete pblockEvict;
|
||||
mapOrphanBlocks.erase(evictHash);
|
||||
printf("ProcessBlock: orphan eviction, %u orphans remain\n", (unsigned int)mapOrphanBlocks.size());
|
||||
}
|
||||
LimitOrphanBlocks(nMaxOrphans);
|
||||
|
||||
// Ask this guy to fill in what we're missing
|
||||
if (pfrom && pindexBest)
|
||||
@@ -3212,9 +3466,10 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
||||
printf("ProcessBlock: ACCEPTED block %d\n", nBestHeight);
|
||||
|
||||
if (pfrom && hashBestHeaderSync != 0)
|
||||
if (hashBestHeaderSync != 0)
|
||||
{
|
||||
const unsigned int nQueued = QueueHeaderSyncBlocks(pfrom, HEADER_DOWNLOAD_WINDOW);
|
||||
// Use parallel queue to distribute across all peers
|
||||
const unsigned int nQueued = QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW);
|
||||
if (nQueued > 0)
|
||||
printf("IBD-DIAG: queued %u more blocks from header planner after accepting %s\n",
|
||||
nQueued, hash.ToString().substr(0,20).c_str());
|
||||
@@ -4394,6 +4649,30 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
// Find the last block the caller has in the main chain
|
||||
CBlockIndex* pindex = locator.GetBlockIndex();
|
||||
|
||||
// Detect incompatible fork: peer sent a locator with entries but
|
||||
// GetBlockIndex() fell through to genesis (no locator hash matched
|
||||
// our main chain). If the peer's tip isn't our genesis,
|
||||
// they're on a completely different fork.
|
||||
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
||||
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
||||
{
|
||||
pfrom->nIncompatibleGetblocks++;
|
||||
if (pfrom->nIncompatibleGetblocks >= 3)
|
||||
{
|
||||
printf("WARNING: peer %s sent %d getblocks with no common blocks — disconnecting (incompatible fork)\n",
|
||||
pfrom->addr.ToString().c_str(), pfrom->nIncompatibleGetblocks);
|
||||
pfrom->Misbehaving(100);
|
||||
return true;
|
||||
}
|
||||
printf("WARNING: peer %s getblocks locator has no common blocks (%d/3 before ban)\n",
|
||||
pfrom->addr.ToString().c_str(), pfrom->nIncompatibleGetblocks);
|
||||
}
|
||||
else if (pindex && pindex != pindexGenesisBlock)
|
||||
{
|
||||
// Peer matched a non-genesis block — they share our chain
|
||||
pfrom->nIncompatibleGetblocks = 0;
|
||||
}
|
||||
|
||||
// Send the rest of the chain
|
||||
if (pindex)
|
||||
pindex = pindex->pnext;
|
||||
@@ -4523,7 +4802,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
|
||||
int nRequested = 0;
|
||||
if (hashBestHeaderSync != 0)
|
||||
nRequested = QueueHeaderSyncBlocks(pfrom, HEADER_DOWNLOAD_WINDOW);
|
||||
nRequested = QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW);
|
||||
|
||||
if (nNewHeaders > 0 || nRequested > 0)
|
||||
printf("IBD-DIAG: accepted %d new headers, queued %d blocks from %zu headers (peer=%s bestHeader=%s)\n",
|
||||
@@ -4631,6 +4910,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);
|
||||
@@ -5165,6 +5447,64 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
||||
pto->PushMessage("inv", vInv);
|
||||
|
||||
|
||||
//
|
||||
// Periodic chain-tip sync: every 45 seconds, ask each peer if they
|
||||
// have blocks we don't. On a small Tor-only network, transient
|
||||
// partitions can cause forks that persist silently — this ensures
|
||||
// nodes discover the longer chain even without explicit announcement.
|
||||
//
|
||||
if (!IsInitialBlockDownload() && !pto->fClient && pindexBest &&
|
||||
GetTime() - pto->nLastTipCheck > 45)
|
||||
{
|
||||
pto->nLastTipCheck = GetTime();
|
||||
pto->pindexLastGetBlocksBegin = NULL; // reset dedup to force request
|
||||
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
|
||||
|
||||
+12
-3
@@ -37,8 +37,9 @@ static const unsigned int MAX_BLOCK_SIZE = 1000000;
|
||||
static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE/2;
|
||||
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 = 750;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS = 2000;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
|
||||
static const unsigned int MAX_REORG_DEPTH = 100; // 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;
|
||||
@@ -59,10 +60,10 @@ static const int fHaveUPnP = false;
|
||||
|
||||
static const uint256 hashGenesisBlockOfficial("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
static const uint256 hashGenesisBlockTestNet ("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 3 * 60 : 10 * 60; }
|
||||
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 90 : 10 * 60; }
|
||||
inline int64_t PastDrift(int64_t nTime, int nHeight) { return nTime - GetMaxTimeDrift(nHeight); }
|
||||
inline int64_t FutureDrift(int64_t nTime, int nHeight) { return nTime + GetMaxTimeDrift(nHeight); }
|
||||
// Height-less overloads always use post-V5.4 rules (3-min drift).
|
||||
// Height-less overloads always use post-V5.4 rules (90-second drift).
|
||||
// All nodes are well past FORK_HEIGHT_V5_4; using the global nBestHeight
|
||||
// here previously caused nodes at different heights to disagree on block
|
||||
// validity during the fork transition — a consensus-splitting bug.
|
||||
@@ -83,6 +84,7 @@ extern uint256 nBestChainTrust;
|
||||
extern uint256 nBestInvalidTrust;
|
||||
extern uint256 hashBestChain;
|
||||
extern CBlockIndex* pindexBest;
|
||||
extern CBlockIndex* pindexFinalized; // auto-checkpoint: deepest finalized block
|
||||
extern unsigned int nTransactionsUpdated;
|
||||
extern uint64_t nLastBlockTx;
|
||||
extern uint64_t nLastBlockSize;
|
||||
@@ -136,6 +138,7 @@ bool IsInitialBlockDownload();
|
||||
std::string GetWarnings(std::string strFor);
|
||||
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock);
|
||||
uint256 WantedByOrphan(const CBlock* pblockOrphan);
|
||||
unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans);
|
||||
const CBlockIndex* GetLastBlockIndex(const CBlockIndex* pindex, bool fProofOfStake);
|
||||
void StakeMiner(CWallet *pwallet);
|
||||
void ResendWalletTransactions(bool fForce = false);
|
||||
@@ -1559,6 +1562,12 @@ public:
|
||||
return vHave.empty();
|
||||
}
|
||||
|
||||
// Return the first hash in the locator (peer's tip), or 0 if empty
|
||||
uint256 GetTipHash() const
|
||||
{
|
||||
return vHave.empty() ? uint256(0) : vHave[0];
|
||||
}
|
||||
|
||||
void Set(const CBlockIndex* pindex)
|
||||
{
|
||||
vHave.clear();
|
||||
|
||||
+14
-3
@@ -413,7 +413,7 @@ void StakeMiner(CWallet *pwallet)
|
||||
if (fTryToSync)
|
||||
{
|
||||
fTryToSync = false;
|
||||
if (vNodes.size() < 1 || nBestHeight < GetNumBlocksOfPeers())
|
||||
if (vNodes.size() < 2 || nBestHeight < GetNumBlocksOfPeers())
|
||||
{
|
||||
MilliSleep(60000);
|
||||
continue;
|
||||
@@ -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);
|
||||
|
||||
+65
-21
@@ -47,7 +47,7 @@ void ThreadOpenAddedConnections2(void* parg);
|
||||
void ThreadMapPort2(void* parg);
|
||||
#endif
|
||||
void ThreadHTTPSeedFetch(void* parg);
|
||||
void ThreadHTTPSeedFetch2(void* parg);
|
||||
bool ThreadHTTPSeedFetch2(void* parg);
|
||||
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
|
||||
|
||||
|
||||
@@ -1381,7 +1381,7 @@ void ThreadOnionSeed(void* parg)
|
||||
|
||||
|
||||
|
||||
// Load hardcoded .onion seeds (if any)
|
||||
// Load hardcoded .onion seeds and queue them for immediate direct connection
|
||||
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
|
||||
int found = 0;
|
||||
|
||||
@@ -1394,23 +1394,50 @@ void ThreadOnionSeed(void* parg)
|
||||
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
|
||||
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay);
|
||||
addrman.Add(addr, parsed);
|
||||
|
||||
// Queue for immediate direct connection (OneShot) — don't wait for
|
||||
// addrman selection which deprioritizes stale timestamps
|
||||
std::string oneShotAddr = std::string(strOnionSeed[seed_idx][0])
|
||||
+ ":" + std::to_string(GetDefaultPort());
|
||||
AddOneShot(oneShotAddr);
|
||||
found++;
|
||||
}
|
||||
|
||||
printf("%d addresses from hardcoded .onion seeds\n", found);
|
||||
printf("%d addresses from hardcoded .onion seeds (queued as OneShot)\n", found);
|
||||
|
||||
// Also fetch dynamic seeds from HTTP seed list
|
||||
// Wait for Tor to establish circuits before attempting HTTPS seed fetch.
|
||||
// The hardcoded OneShot connections can race ahead meanwhile.
|
||||
printf("ThreadOnionSeed: waiting 20s for Tor circuits before HTTPS seed fetch...\n");
|
||||
for (int i = 0; i < 20 && !fShutdown; i++)
|
||||
MilliSleep(1000);
|
||||
|
||||
// Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff.
|
||||
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
{
|
||||
bool ok = false;
|
||||
int delays[] = {0, 30, 60, 120};
|
||||
for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) {
|
||||
if (attempt > 0) {
|
||||
printf("ThreadOnionSeed: HTTPS seed fetch retry %d in %ds...\n", attempt, delays[attempt]);
|
||||
for (int i = 0; i < delays[attempt] && !fShutdown; i++)
|
||||
MilliSleep(1000);
|
||||
}
|
||||
if (!fShutdown)
|
||||
ok = ThreadHTTPSeedFetch2(NULL);
|
||||
}
|
||||
if (!ok && !fShutdown)
|
||||
printf("ThreadOnionSeed: all HTTPS seed fetch attempts failed\n");
|
||||
}
|
||||
|
||||
printf("ThreadOnionSeed: initial seeding complete\n");
|
||||
|
||||
// Periodic re-seeding: if the node becomes isolated (0 outbound peers),
|
||||
// re-fetch the seed list. Check every 10 minutes, re-seed at most once
|
||||
// per 30 minutes to avoid hammering the seed server.
|
||||
// Periodic re-seeding for isolated or under-connected nodes.
|
||||
// Check every 2 minutes, re-seed when < 2 outbound peers.
|
||||
// First re-seed after 5 min cooldown, then 15 min for subsequent.
|
||||
int64_t nLastReseed = GetTime();
|
||||
bool bFirstReseed = true;
|
||||
while (!fShutdown) {
|
||||
for (int i = 0; i < 600 && !fShutdown; i++) // sleep 10 minutes
|
||||
for (int i = 0; i < 120 && !fShutdown; i++) // sleep 2 minutes
|
||||
MilliSleep(1000);
|
||||
|
||||
if (fShutdown) break;
|
||||
@@ -1423,10 +1450,20 @@ void ThreadOnionSeed(void* parg)
|
||||
nOutbound++;
|
||||
}
|
||||
|
||||
if (nOutbound == 0 && GetTime() - nLastReseed > 30 * 60) {
|
||||
printf("ThreadOnionSeed: no outbound peers, re-seeding...\n");
|
||||
int64_t nCooldown = bFirstReseed ? 5 * 60 : 15 * 60;
|
||||
if (nOutbound < 2 && GetTime() - nLastReseed > nCooldown) {
|
||||
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
|
||||
// Also re-queue hardcoded seeds for direct connection
|
||||
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != NULL; seed_idx++) {
|
||||
std::string oneShotAddr = std::string(strOnionSeed[seed_idx][0])
|
||||
+ ":" + std::to_string(GetDefaultPort());
|
||||
AddOneShot(oneShotAddr);
|
||||
}
|
||||
|
||||
nLastReseed = GetTime();
|
||||
bFirstReseed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1486,7 +1523,7 @@ void ThreadDumpAddress(void* parg)
|
||||
printf("ThreadDumpAddress exited\n");
|
||||
}
|
||||
|
||||
void ThreadHTTPSeedFetch2(void* parg)
|
||||
bool ThreadHTTPSeedFetch2(void* parg)
|
||||
{
|
||||
static const char* DEFAULT_SEED_URL_HOST = "seeds.cryptographic-triangles.org";
|
||||
static const char* DEFAULT_SEED_URL_PATH = "/seeds.txt";
|
||||
@@ -1515,7 +1552,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
if (!ConnectSocketByName(addrResolved, hSocket, connectDest.c_str(), HTTPS_PORT, nConnectTimeout)) {
|
||||
printf("HTTPS seed fetch: cannot connect to %s through Tor proxy\n", seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up TLS over the connected socket
|
||||
@@ -1523,7 +1560,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
if (!ctx) {
|
||||
printf("HTTPS seed fetch: SSL_CTX_new failed\n");
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use system default CA certificates for verification
|
||||
@@ -1535,7 +1572,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
printf("HTTPS seed fetch: SSL_new failed\n");
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set SNI hostname (required for Caddy/Let's Encrypt)
|
||||
@@ -1552,7 +1589,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("HTTPS seed fetch: TLS connection established to %s\n", seedHost.c_str());
|
||||
@@ -1575,7 +1612,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
nSent += nBytes;
|
||||
}
|
||||
@@ -1600,21 +1637,21 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
if (response.empty()) {
|
||||
printf("HTTPS seed fetch: empty response from %s\n", seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse HTTP response - find end of headers
|
||||
size_t headerEnd = response.find("\r\n\r\n");
|
||||
if (headerEnd == std::string::npos) {
|
||||
printf("HTTPS seed fetch: malformed response (no header terminator)\n");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check status code
|
||||
std::string statusLine = response.substr(0, response.find("\r\n"));
|
||||
if (statusLine.find("200") == std::string::npos) {
|
||||
printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string body = response.substr(headerEnd + 4);
|
||||
@@ -1626,7 +1663,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
while (std::getline(lines, line))
|
||||
{
|
||||
if (fShutdown)
|
||||
return;
|
||||
return false;
|
||||
|
||||
// Trim whitespace and carriage returns
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||
@@ -1667,17 +1704,24 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
CAddress addr(CService(parsed, port));
|
||||
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
|
||||
addrman.Add(addr, CNetAddr("https-seed", true));
|
||||
// Queue the first 8 seeds for immediate direct connection
|
||||
if (found < 8) {
|
||||
std::string oneShotAddr = addrStr + ":" + std::to_string(port);
|
||||
AddOneShot(oneShotAddr);
|
||||
}
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
|
||||
return found > 0;
|
||||
|
||||
} catch (std::exception& e) {
|
||||
printf("HTTPS seed fetch failed: %s\n", e.what());
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
if (ctx) SSL_CTX_free(ctx);
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,10 @@ public:
|
||||
CBlockIndex* pindexLastGetHeadersBegin;
|
||||
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
|
||||
int nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection)
|
||||
|
||||
// flood relay
|
||||
std::vector<CAddress> vAddrToSend;
|
||||
@@ -319,6 +323,10 @@ public:
|
||||
pindexLastGetHeadersBegin = 0;
|
||||
hashLastGetHeadersEnd = 0;
|
||||
nStartingHeight = -1;
|
||||
nLastTipCheck = 0;
|
||||
nAvgBlockLatencyUs = 0;
|
||||
nBlocksDelivered = 0;
|
||||
nIncompatibleGetblocks = 0;
|
||||
fGetAddr = false;
|
||||
nMisbehavior = 0;
|
||||
hashCheckpointKnown = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Hardcoded onion seed nodes for initial peer discovery.
|
||||
// Also fetched dynamically via https://seeds.cryptographic-triangles.org/seeds.txt
|
||||
static const char *strMainNetOnionSeed[][1] = {
|
||||
{"6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion"},
|
||||
{"jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion"},
|
||||
{"uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion"},
|
||||
{"el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion"},
|
||||
|
||||
@@ -28,6 +28,9 @@ double GetDifficulty(const CBlockIndex* blockindex)
|
||||
blockindex = GetLastBlockIndex(pindexBest, false);
|
||||
}
|
||||
|
||||
if (blockindex == NULL)
|
||||
return 1.0;
|
||||
|
||||
int nShift = (blockindex->nBits >> 24) & 0xff;
|
||||
|
||||
double dDiff =
|
||||
@@ -360,6 +363,49 @@ Value gettxoutsetinfo(const Array& params, bool fHelp)
|
||||
return obj;
|
||||
}
|
||||
|
||||
Value recalculatesupply(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"recalculatesupply\n"
|
||||
"Recalculates the money supply by summing all unspent transaction outputs.\n"
|
||||
"Updates the stored money supply value at the chain tip and persists it to disk.\n"
|
||||
"Returns the old and new supply values for comparison.\n"
|
||||
"\nWARNING: This modifies blockchain index state. Only use if money supply is incorrect.");
|
||||
|
||||
if (!pindexBest)
|
||||
throw runtime_error("recalculatesupply: no best block");
|
||||
|
||||
int nUtxoCount = 0;
|
||||
CTxDB txdb;
|
||||
int64_t nCalculatedSupply = txdb.SumUtxoValues(nUtxoCount);
|
||||
int64_t nOldSupply = pindexBest->nMoneySupply;
|
||||
int64_t nDifference = nCalculatedSupply - nOldSupply;
|
||||
|
||||
// Sanity check: difference should be reasonable (not millions of TRI)
|
||||
// Max supply is 2,222,222 TRI, so any difference > 1M TRI is suspicious
|
||||
if (abs64(nDifference) > 1000000 * COIN)
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: calculated supply differs by %s TRI - this is abnormal, refusing to update",
|
||||
FormatMoney(abs64(nDifference)).c_str()));
|
||||
|
||||
// Update the chain tip's money supply
|
||||
pindexBest->nMoneySupply = nCalculatedSupply;
|
||||
|
||||
// Persist to LevelDB
|
||||
CTxDB txdbWrite;
|
||||
if (!txdbWrite.WriteBlockIndex(CDiskBlockIndex(pindexBest)))
|
||||
throw runtime_error("recalculatesupply: failed to write updated block index");
|
||||
|
||||
Object result;
|
||||
result.push_back(Pair("height", (int)nBestHeight));
|
||||
result.push_back(Pair("old_supply", ValueFromAmount(nOldSupply)));
|
||||
result.push_back(Pair("new_supply", ValueFromAmount(nCalculatedSupply)));
|
||||
result.push_back(Pair("difference", ValueFromAmount(nDifference)));
|
||||
result.push_back(Pair("utxo_count", nUtxoCount));
|
||||
return result;
|
||||
}
|
||||
|
||||
// triangles: get information of sync-checkpoint
|
||||
Value getcheckpoint(const Array& params, bool fHelp)
|
||||
{
|
||||
@@ -416,6 +462,48 @@ Value getblockchaininfo(const Array& params, bool fHelp)
|
||||
return obj;
|
||||
}
|
||||
|
||||
Value gencheckpoints(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() > 1)
|
||||
throw runtime_error(
|
||||
"gencheckpoints [interval]\n"
|
||||
"Generates hardcoded checkpoint entries for checkpoints.cpp.\n"
|
||||
"Outputs C++ map entries for every <interval> blocks (default 5000)\n"
|
||||
"from genesis to current tip, ready to paste into the source code.");
|
||||
|
||||
int nInterval = 5000;
|
||||
if (params.size() > 0)
|
||||
nInterval = params[0].get_int();
|
||||
if (nInterval < 1)
|
||||
throw runtime_error("Interval must be >= 1");
|
||||
|
||||
std::string result;
|
||||
result += "// Generated by gencheckpoints RPC at height " + std::to_string(nBestHeight) + "\n";
|
||||
result += "static MapCheckpoints mapCheckpoints = {\n";
|
||||
|
||||
// Always include genesis
|
||||
CBlockIndex* pindex = mapBlockIndex[hashBestChain];
|
||||
while (pindex->pprev)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
bool first = true;
|
||||
while (pindex)
|
||||
{
|
||||
if (pindex->nHeight % nInterval == 0 || pindex->nHeight == nBestHeight)
|
||||
{
|
||||
if (!first)
|
||||
result += ",\n";
|
||||
result += " {" + std::to_string(pindex->nHeight) + ", uint256(\"0x"
|
||||
+ pindex->GetBlockHash().GetHex() + "\")}";
|
||||
first = false;
|
||||
}
|
||||
pindex = pindex->pnext;
|
||||
}
|
||||
result += "\n};\n";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Address index RPC commands
|
||||
// ============================================================================
|
||||
|
||||
+32
-1
@@ -78,7 +78,8 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"getstakinginfo\n"
|
||||
"Returns an object containing staking-related information.");
|
||||
"Returns an object containing staking-related information.\n"
|
||||
"Includes diagnostic details about why staking may be disabled.");
|
||||
|
||||
uint64_t nMinWeight = 0, nMaxWeight = 0, nWeight = 0;
|
||||
pwalletMain->GetStakeWeight(*pwalletMain, nMinWeight, nMaxWeight, nWeight);
|
||||
@@ -87,6 +88,26 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
bool staking = nLastCoinStakeSearchInterval && nWeight;
|
||||
int nExpectedTime = staking ? (nTargetSpacing * nNetworkWeight / nWeight) : -1;
|
||||
|
||||
// Diagnostic: determine why staking might be disabled
|
||||
Array stakingDisabledReasons;
|
||||
if (!GetBoolArg("-staking", true))
|
||||
stakingDisabledReasons.push_back("staking disabled via -staking=0 flag");
|
||||
|
||||
if (pwalletMain->IsLocked())
|
||||
stakingDisabledReasons.push_back("wallet is locked (use walletpassphrase <pw> <timeout> true)");
|
||||
|
||||
if (vNodes.empty())
|
||||
stakingDisabledReasons.push_back("no network connections (need at least 1 peer)");
|
||||
|
||||
if (IsInitialBlockDownload())
|
||||
stakingDisabledReasons.push_back("initial block download in progress");
|
||||
|
||||
if (nWeight == 0)
|
||||
stakingDisabledReasons.push_back("no mature coins available (coins need 520 confirmations)");
|
||||
|
||||
if (!staking && nLastCoinStakeSearchInterval == 0)
|
||||
stakingDisabledReasons.push_back("stake miner thread not running");
|
||||
|
||||
Object obj;
|
||||
|
||||
obj.push_back(Pair("enabled", GetBoolArg("-staking", true)));
|
||||
@@ -105,6 +126,16 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
|
||||
obj.push_back(Pair("expectedtime", nExpectedTime));
|
||||
|
||||
// Add detailed diagnostics
|
||||
obj.push_back(Pair("walletlocked", pwalletMain->IsLocked()));
|
||||
obj.push_back(Pair("walletunlockedforstakingonly", fWalletUnlockStakingOnly));
|
||||
obj.push_back(Pair("connections", (int)vNodes.size()));
|
||||
obj.push_back(Pair("initialblockdownload", IsInitialBlockDownload()));
|
||||
obj.push_back(Pair("maturecoins", nWeight > 0));
|
||||
|
||||
if (!stakingDisabledReasons.empty())
|
||||
obj.push_back(Pair("staking_disabled_reasons", stakingDisabledReasons));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
+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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -314,9 +314,11 @@ static const CRPCCommand vRPCCommands[] =
|
||||
{ "signrawtransaction", &signrawtransaction, false, false },
|
||||
{ "sendrawtransaction", &sendrawtransaction, false, false },
|
||||
{ "getcheckpoint", &getcheckpoint, true, false },
|
||||
{ "gencheckpoints", &gencheckpoints, true, false },
|
||||
{ "getchaintips", &getchaintips, true, false },
|
||||
{ "invalidateblock", &invalidateblock, false, false },
|
||||
{ "reconsiderblock", &reconsiderblock, false, false },
|
||||
{ "recalculatesupply", &recalculatesupply, false, false },
|
||||
{ "reservebalance", &reservebalance, false, true},
|
||||
{ "checkwallet", &checkwallet, false, true},
|
||||
{ "repairwallet", &repairwallet, false, true},
|
||||
|
||||
@@ -222,9 +222,11 @@ extern json_spirit::Value getblockhash(const json_spirit::Array& params, bool fH
|
||||
extern json_spirit::Value getblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value recalculatesupply(const json_spirit::Array& params, bool fHelp);
|
||||
|
||||
extern json_spirit::Value getaddressbalance(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getaddressutxos(const json_spirit::Array& params, bool fHelp);
|
||||
|
||||
+128
-2
@@ -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>
|
||||
@@ -162,7 +163,9 @@ bool CTxDB::TxnCommit()
|
||||
delete activeBatch;
|
||||
activeBatch = NULL;
|
||||
if (!status.ok()) {
|
||||
printf("LevelDB batch commit failure: %s\n", status.ToString().c_str());
|
||||
printf("ERROR: LevelDB batch commit failure: %s\n", status.ToString().c_str());
|
||||
printf("ERROR: This may indicate disk full, corruption, or permissions issue.\n");
|
||||
printf("ERROR: Chain state may be inconsistent - immediate investigation required!\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -870,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;
|
||||
|
||||
@@ -904,3 +998,35 @@ bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t CTxDB::SumUtxoValues(int& nCount)
|
||||
{
|
||||
nCount = 0;
|
||||
int64_t nTotal = 0;
|
||||
|
||||
// Seek to the start of UTXO entries (key prefix "u")
|
||||
CDataStream ssKeyPrefix(SER_DISK, CLIENT_VERSION);
|
||||
ssKeyPrefix << make_pair(string("u"), make_pair(uint256(0), (unsigned int)0));
|
||||
std::string strPrefixBegin = ssKeyPrefix.str();
|
||||
|
||||
leveldb::Iterator* it = pdb->NewIterator(leveldb::ReadOptions());
|
||||
for (it->Seek(strPrefixBegin); it->Valid(); it->Next())
|
||||
{
|
||||
// Check key prefix is still "u"
|
||||
CDataStream ssKey(it->key().data(), it->key().data() + it->key().size(), SER_DISK, CLIENT_VERSION);
|
||||
std::string strKeyType;
|
||||
ssKey >> strKeyType;
|
||||
if (strKeyType != "u")
|
||||
break;
|
||||
|
||||
// Deserialize the UTXO entry and sum the value
|
||||
CDataStream ssValue(it->value().data(), it->value().data() + it->value().size(), SER_DISK, CLIENT_VERSION);
|
||||
CUtxoEntry entry;
|
||||
ssValue >> entry;
|
||||
|
||||
nTotal += entry.nValue;
|
||||
nCount++;
|
||||
}
|
||||
delete it;
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ public:
|
||||
bool WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry);
|
||||
bool EraseUtxo(const uint256& hash, unsigned int n);
|
||||
bool HaveUtxo(const uint256& hash, unsigned int n);
|
||||
int64_t SumUtxoValues(int& nCount);
|
||||
|
||||
private:
|
||||
bool LoadBlockIndexGuts();
|
||||
|
||||
+2
-2
@@ -41,10 +41,10 @@ const std::string CLIENT_NAME("Cryptographic Triangles");
|
||||
#endif
|
||||
|
||||
#define BUILD_DESC_FROM_COMMIT(maj,min,rev,build,commit) \
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "." DO_STRINGIZE(build) "" commit
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "" commit
|
||||
|
||||
#define BUILD_DESC_FROM_UNKNOWN(maj,min,rev,build) \
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "." DO_STRINGIZE(build) ""
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) ""
|
||||
|
||||
#ifndef BUILD_DESC
|
||||
# ifdef GIT_COMMIT_ID
|
||||
|
||||
@@ -16,6 +16,40 @@ namespace fs = boost::filesystem;
|
||||
static uint64_t nAccountingEntryNumber = 0;
|
||||
extern bool fWalletUnlockStakingOnly;
|
||||
|
||||
//
|
||||
// Auto-backup wallet before flush/rewrite operations.
|
||||
// Copies wallet.dat to wallet.dat.auto.bak if the backup is older than the wallet.
|
||||
// Returns true if backup was created or already up to date.
|
||||
//
|
||||
bool AutoBackupWallet(const fs::path& walletPath)
|
||||
{
|
||||
fs::path backupPath = walletPath.string() + ".auto.bak";
|
||||
try {
|
||||
// Only back up if wallet exists and is non-trivial (>1KB)
|
||||
if (!fs::exists(walletPath))
|
||||
return true;
|
||||
uintmax_t walletSize = fs::file_size(walletPath);
|
||||
if (walletSize < 1024) {
|
||||
printf("AutoBackupWallet: wallet.dat is only %llu bytes (possibly corrupt), skipping auto-backup\n",
|
||||
(unsigned long long)walletSize);
|
||||
return false;
|
||||
}
|
||||
// Skip if backup exists and is same size (already backed up this version)
|
||||
if (fs::exists(backupPath)) {
|
||||
uintmax_t backupSize = fs::file_size(backupPath);
|
||||
if (backupSize == walletSize)
|
||||
return true;
|
||||
}
|
||||
fs::copy_file(walletPath, backupPath, fs::copy_options::overwrite_existing);
|
||||
printf("AutoBackupWallet: backed up wallet.dat (%llu bytes) to wallet.dat.auto.bak\n",
|
||||
(unsigned long long)walletSize);
|
||||
return true;
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
printf("AutoBackupWallet: failed - %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// CWalletDB
|
||||
//
|
||||
@@ -580,6 +614,10 @@ void ThreadFlushWalletDB(void* parg)
|
||||
nLastFlushed = nWalletDBUpdated;
|
||||
int64_t nStart = GetTimeMillis();
|
||||
|
||||
// Auto-backup before flush (protects against corruption)
|
||||
fs::path walletPath = GetDataDir() / strFile;
|
||||
AutoBackupWallet(walletPath);
|
||||
|
||||
// Flush wallet.dat so it's self contained
|
||||
bitdb.CloseDb(strFile);
|
||||
bitdb.CheckpointLSN(strFile);
|
||||
|
||||
Reference in New Issue
Block a user