Compare commits

...

8 Commits

Author SHA1 Message Date
sami7777 0df054bbcb Build acceleration: ccache, unity build, precompiled headers
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
- Auto-detect and use ccache as compiler launcher when available
- Add ENABLE_UNITY_BUILD option for jumbo builds (batch size 8)
- Precompile heavy STL/Boost/OpenSSL headers for C++ targets
- Exclude hash9 crypto from unity builds (colliding static symbols)
- Fix RAND_screen() compile error on OpenSSL 3.x (removed API)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 13:28:02 -07:00
sami7777 fbd931a392 Network stability & connectivity hardening (v5.9.0)
- BIP 31 ping/pong with 2-min heartbeat, RTT tracking, 3-miss disconnect
- Reduce max outbound from 16 to 8, add -maxoutbound flag
- Emergency reconnection: 15s re-seed when 0 peers, 30s when 1 peer
- Inactivity timeout reduced from 90min to 10min (dead peer detection)
- Header sync TTL extended from 5min to 15min for Tor latency
- Reserve 2 inbound slots for known seed nodes at capacity
- Enhanced address gossip: hourly rebroadcast, getaddr from all peers
- New getnetworkstability RPC with isolation risk assessment
- getpeerinfo now includes pingtime, blocksdelivered, avglatency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 02:52:33 -07:00
sami7777 a792f90489 Fix linker error: move extern txdb declaration out of UtxoSnapshot namespace
The extern declaration for the global leveldb::DB *txdb was inside
namespace UtxoSnapshot{}, causing the linker to look for
UtxoSnapshot::txdb instead of the global ::txdb defined in
txdb-leveldb.cpp.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 02:39:37 -07:00
sami7777 64939a9793 Sync & relay improvements: compact blocks, sendheaders, adaptive timeouts (v5.8.8)
7 sync/relay optimizations for faster block propagation on Tor-only network:

1. Improved unsolicited block push: track nBestKnownHeight from inv/block
   messages instead of static nStartingHeight, so peers that sync up
   receive direct block pushes
2. Reduced redundant-request timeout from 20s to 5s for faster failover
3. Pipeline improvement: continuous download window refill after every
   accepted block + refill interval reduced from 5000 to 500 blocks
4. Sendheaders (BIP 130-style): negotiate header-based block announcements
   to save one round-trip vs inv->getdata->block
5. Compact block relay: send header + prefilled coinbase/coinstake + short
   tx IDs. For typical PoS blocks (0-2 txs) this is the complete block
   with no follow-up needed. Includes getblocktxn/blocktxn for missing txs
6. Adaptive peer timeouts: use rolling average latency (EMA 7/8) to set
   per-peer request and stall timeouts instead of fixed constants
7. Dual-peer requesting during IBD: request each block from two peers
   simultaneously, use whichever arrives first

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 02:23:32 -07:00
sami7777 b506a48192 UTXO snapshot support, script verify cache, and Tor sync tuning (v5.8.7)
- Add UTXO snapshot dump/load system (utxosnapshot.cpp/h) for fast initial sync
- Add dumputxoset RPC command to create snapshots from current chain state
- Add script verification cache (sigcache.h) to skip re-verifying scripts
  already validated during mempool acceptance
- Bootstrap: try UTXO snapshot first (fast path), fall back to full bootstrap
- Support manual utxo-snapshot.bin loading on startup
- Tune sync parameters for Tor: increase timeouts, reduce buffer sizes
- Header sync cache: TTL-based eviction instead of full cache clear
- Reduce orphan block limits and script check batch size for lower memory usage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 01:17:51 -07:00
sami7777 dee0d9ef62 Fix Tor process cleanup: kill orphans on startup, Job Object on Windows
- Add Windows Job Object (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) so Tor
  child process is automatically killed when the wallet exits for any
  reason (crash, Task Manager, clean shutdown)
- Replace port-reuse "assume running" path with active orphan cleanup:
  Windows enumerates and kills tor.exe processes, Linux uses PID file
- Move deep-reorg trust-delta check into Reorganize() so short forks
  (<=6 blocks) converge freely while long-range attacks are still blocked

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 22:05:09 -07:00
sami7777 22e220acaa Anti-fork hardening + checkpoint update (v5.8.6)
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
- Add checkpoints through block 2,209,000 to lock canonical chain
- Ban peers on incompatible forks (no common blocks after 3 getblocks)
- Auto-checkpoint: finalize blocks at MAX_REORG_DEPTH to prevent deep reorgs
- Require 10% trust delta for side-chain reorgs (first-seen advantage)
- Add gencheckpoints RPC command for easy future checkpoint generation
- Add wallet onion address to hardcoded seed list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 18:14:58 -07:00
sami7777 1c068f4782 Sync speed + anti-fork hardening (v5.8.5)
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
- In-memory UTXO cache (2M entries, read-through with negative caching)
- Signature cache upgrade (unordered_set, 200K entries, 64-bit compact keys)
- Speed-weighted peer block assignment (fast peers get more blocks)
- 500-block max reorg depth (finality limit post-IBD)
- 7-day coin age soft cap (prevents stake surprise attacks)
- Timestamp tiebreaker for equal-trust fork resolution
- 30s stake cooldown after orphaned block (reduces fork oscillation)
- Slow-peer eviction (disconnect 0-block peers after 3min during sync)
- Only push new blocks to near-tip peers (within 10 blocks)
- Smart orphan eviction (FIFO oldest-first instead of random)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 11:55:18 -07:00
26 changed files with 2322 additions and 232 deletions
+22 -1
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif()
project(Triangles
VERSION 5.8.3
VERSION 5.9.0
DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX
)
@@ -17,6 +17,24 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_C_STANDARD 11)
# ── Build acceleration ──
# ccache: auto-detect and use if available
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "ccache found: ${CCACHE_PROGRAM}")
else()
message(STATUS "ccache not found — install it for faster rebuilds")
endif()
# Unity (jumbo) build: batch source files to reduce header parsing overhead
option(ENABLE_UNITY_BUILD "Enable CMake unity (jumbo) builds" OFF)
if(ENABLE_UNITY_BUILD)
set(CMAKE_UNITY_BUILD ON)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
endif()
# ── Output directories ──
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
@@ -113,4 +131,7 @@ message(STATUS " D-Bus: ${USE_DBUS}")
message(STATUS " ZMQ: ${USE_ZMQ}")
message(STATUS " Embedded Tor: ${USE_TOR_EMBEDDED}")
message(STATUS " Static linking: ${ENABLE_STATIC}")
message(STATUS " ccache: ${CCACHE_PROGRAM}")
message(STATUS " Unity build: ${ENABLE_UNITY_BUILD}")
message(STATUS " Precompiled header: ON")
message(STATUS "")
+29
View File
@@ -23,6 +23,8 @@ add_library(hash9_crypto STATIC
)
target_include_directories(hash9_crypto PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
set_target_properties(hash9_crypto PROPERTIES LINKER_LANGUAGE C)
# Hash9 C files have colliding static symbols (IV512, DECL_STATE, etc.) — skip unity
set_target_properties(hash9_crypto PROPERTIES UNITY_BUILD OFF)
# ═══════════════════════════════════════════════════════════════════════════════
# 2. JSON library (header-only nlohmann/json via json_compat.h shim)
@@ -72,6 +74,7 @@ set(CORE_SOURCES
rpcsmessage.cpp
zmqpublishnotifier.cpp
txdb-leveldb.cpp
utxosnapshot.cpp
lz4/lz4.c
tor/onion_v3.cpp
tor/tor_process.cpp
@@ -184,6 +187,31 @@ endif()
add_dependencies(triangles_common generate_build_info build_leveldb)
# ── Precompiled header (heavy STL + Boost + OpenSSL includes, C++ only) ──
target_precompile_headers(triangles_common PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:<string$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<vector$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<map$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<deque$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<algorithm$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<sstream$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<stdexcept$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<cstdint$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<cstring$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<memory$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<functional$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/filesystem.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/filesystem/fstream.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread/mutex.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread/condition_variable.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<boost/algorithm/string.hpp$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/sha.h$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/crypto.h$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/rand.h$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/evp.h$<ANGLE-R>>"
)
# ═══════════════════════════════════════════════════════════════════════════════
# 4. Headless daemon (trianglesd)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -195,6 +223,7 @@ if(BUILD_DAEMON)
)
# No QT_GUI define — daemon gets the #if !defined(QT_GUI) code paths
target_link_libraries(trianglesd PRIVATE triangles_common)
target_precompile_headers(trianglesd REUSE_FROM triangles_common)
if(WIN32)
set_target_properties(trianglesd PROPERTIES SUFFIX ".exe")
+191 -66
View File
@@ -2,6 +2,7 @@
// Distributed under the MIT/X11 software license
#include "bootstrap.h"
#include "utxosnapshot.h"
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
@@ -14,6 +15,9 @@
#include "netbase.h"
#include "net.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <fstream>
#include <sstream>
#include <cstdio>
@@ -42,34 +46,6 @@ bool NeedsBootstrap(const fs::path& dataDir)
return !fs::exists(dataDir / "blk0001.dat");
}
// Send all bytes on a raw socket
static bool SendAll(SOCKET sock, const char* data, size_t len)
{
while (len > 0) {
int n = send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
if (n <= 0) return false;
data += n;
len -= n;
}
return true;
}
// Read until `delim` found in received data. Returns data including delimiter.
static bool RecvUntil(SOCKET sock, std::string& out, const std::string& delim)
{
out.clear();
char c;
while (true) {
int n = recv(sock, &c, 1, 0);
if (n <= 0) return false;
out += c;
if (out.size() >= delim.size() &&
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
return true;
if (out.size() > 64 * 1024) return false; // header too large
}
}
// Direct TCP connection bypassing Tor SOCKS proxy.
// Used for bootstrap downloads where the server is on clearnet.
static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& strError)
@@ -106,6 +82,128 @@ static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& s
return hSocket;
}
// RAII wrapper for an HTTP(S) connection (socket + optional TLS)
struct HttpConn {
SOCKET sock;
SSL_CTX* ctx;
SSL* ssl;
HttpConn() : sock(INVALID_SOCKET), ctx(nullptr), ssl(nullptr) {}
~HttpConn() { Close(); }
void Close() {
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; }
if (ctx) { SSL_CTX_free(ctx); ctx = nullptr; }
if (sock != INVALID_SOCKET) { closesocket(sock); sock = INVALID_SOCKET; }
}
bool Send(const char* data, size_t len) {
while (len > 0) {
int n = ssl ? SSL_write(ssl, data, (int)std::min(len, (size_t)65536))
: send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
if (n <= 0) return false;
data += n;
len -= n;
}
return true;
}
int Recv(char* buf, int len) {
return ssl ? SSL_read(ssl, buf, len) : recv(sock, buf, len, 0);
}
// Read until delimiter found. Returns data including delimiter.
bool RecvUntil(std::string& out, const std::string& delim) {
out.clear();
char c;
while (true) {
int n = Recv(&c, 1);
if (n <= 0) return false;
out += c;
if (out.size() >= delim.size() &&
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
return true;
if (out.size() > 64 * 1024) return false; // header too large
}
}
// Establish TLS on an already-connected socket
bool StartTLS(const std::string& hostname, std::string& strError) {
ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
strError = "Failed to create SSL context";
return false;
}
// Skip cert verification — we verify data integrity via checkpoint hashes
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
ssl = SSL_new(ctx);
if (!ssl) {
strError = "Failed to create SSL object";
return false;
}
SSL_set_fd(ssl, (int)sock);
SSL_set_tlsext_host_name(ssl, hostname.c_str()); // SNI
if (SSL_connect(ssl) != 1) {
unsigned long err = ERR_get_error();
char errBuf[256];
ERR_error_string_n(err, errBuf, sizeof(errBuf));
strError = "TLS handshake failed with " + hostname + ": " + errBuf;
return false;
}
return true;
}
};
// Parse host, port, and path from an absolute URL.
// Sets useSSL, host, port, path. Returns false for unsupported schemes.
static bool ParseAbsoluteUrl(const std::string& url,
bool& useSSL, std::string& host,
int& port, std::string& path)
{
if (url.compare(0, 8, "https://") == 0) {
useSSL = true;
std::string rest = url.substr(8);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
host = rest.substr(0, pathStart);
path = rest.substr(pathStart);
} else {
host = rest;
path = "/";
}
size_t colonPos = host.find(':');
if (colonPos != std::string::npos) {
port = std::atoi(host.c_str() + colonPos + 1);
host = host.substr(0, colonPos);
} else {
port = 443;
}
return true;
} else if (url.compare(0, 7, "http://") == 0) {
useSSL = false;
std::string rest = url.substr(7);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
host = rest.substr(0, pathStart);
path = rest.substr(pathStart);
} else {
host = rest;
path = "/";
}
size_t colonPos = host.find(':');
if (colonPos != std::string::npos) {
port = std::atoi(host.c_str() + colonPos + 1);
host = host.substr(0, colonPos);
} else {
port = 80;
}
return true;
}
return false;
}
bool DownloadFile(const std::string& host, const std::string& urlPath,
const fs::path& destPath,
ProgressCallback progressFn,
@@ -115,25 +213,38 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
try {
std::string currentHost = host;
std::string currentPath = urlPath;
SOCKET hSocket = INVALID_SOCKET;
int currentPort = PORT;
bool useSSL = false;
std::string headerData;
int redirectCount = 0;
const int MAX_REDIRECTS = 5;
HttpConn conn;
// Connection + redirect loop
while (true) {
conn.Close(); // clean slate for each attempt
if (noProxy) {
hSocket = ConnectDirectTCP(currentHost, PORT, strError);
if (hSocket == INVALID_SOCKET)
conn.sock = ConnectDirectTCP(currentHost, currentPort, strError);
if (conn.sock == INVALID_SOCKET)
return false;
} else {
CService addr;
if (!ConnectSocketByName(addr, hSocket, currentHost.c_str(), PORT, 30)) {
if (!ConnectSocketByName(addr, conn.sock, currentHost.c_str(), currentPort, 30)) {
strError = "Cannot connect to " + currentHost + " (check Tor proxy)";
return false;
}
}
// Establish TLS when needed
if (useSSL) {
if (!conn.StartTLS(currentHost, strError))
return false;
printf("Bootstrap: TLS established with %s:%d\n",
currentHost.c_str(), currentPort);
}
// Send HTTP GET request
std::string request =
"GET " + currentPath + " HTTP/1.1\r\n"
@@ -142,15 +253,13 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
"User-Agent: Triangles\r\n"
"\r\n";
if (!SendAll(hSocket, request.data(), request.size())) {
closesocket(hSocket);
if (!conn.Send(request.data(), request.size())) {
strError = "Failed to send request to " + currentHost;
return false;
}
// Read response headers
if (!RecvUntil(hSocket, headerData, "\r\n\r\n")) {
closesocket(hSocket);
if (!conn.RecvUntil(headerData, "\r\n\r\n")) {
strError = "Failed to read HTTP headers from " + currentHost;
return false;
}
@@ -164,8 +273,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
// Handle HTTP redirects
if (status_code == 301 || status_code == 302 ||
status_code == 307 || status_code == 308) {
closesocket(hSocket);
hSocket = INVALID_SOCKET;
if (++redirectCount > MAX_REDIRECTS) {
strError = "Too many redirects for " + urlPath;
@@ -193,28 +300,14 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
location = headerData.substr(valStart);
boost::trim(location);
// Reject HTTPS redirects (no TLS support)
if (location.compare(0, 8, "https://") == 0) {
strError = "Server redirected to HTTPS (not supported). "
"Configure bootstrap server for plain HTTP.";
return false;
}
// Parse redirect URL
if (location.compare(0, 7, "http://") == 0) {
std::string rest = location.substr(7);
size_t pathStart = rest.find('/');
if (pathStart != std::string::npos) {
currentHost = rest.substr(0, pathStart);
currentPath = rest.substr(pathStart);
} else {
currentHost = rest;
currentPath = "/";
// Parse redirect URL — supports http://, https://, and relative paths
if (location.compare(0, 7, "http://") == 0 ||
location.compare(0, 8, "https://") == 0) {
if (!ParseAbsoluteUrl(location, useSSL, currentHost,
currentPort, currentPath)) {
strError = "Unsupported redirect location: " + location;
return false;
}
// Strip port from host if present
size_t colonPos = currentHost.find(':');
if (colonPos != std::string::npos)
currentHost = currentHost.substr(0, colonPos);
} else if (!location.empty() && location[0] == '/') {
currentPath = location;
} else {
@@ -222,13 +315,13 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
return false;
}
printf("Bootstrap: redirect %d -> %s%s\n",
status_code, currentHost.c_str(), currentPath.c_str());
printf("Bootstrap: redirect %d -> %s%s%s (port %d)\n",
status_code, useSSL ? "https://" : "http://",
currentHost.c_str(), currentPath.c_str(), currentPort);
continue;
}
if (status_code != 200) {
closesocket(hSocket);
strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath;
return false;
}
@@ -252,7 +345,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
// Open output file
FILE* file = fopen(destPath.string().c_str(), "wb");
if (!file) {
closesocket(hSocket);
strError = "Cannot create file: " + destPath.string();
return false;
}
@@ -263,10 +355,9 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
char chunk[65536];
while (true) {
int n = recv(hSocket, chunk, sizeof(chunk), 0);
int n = conn.Recv(chunk, sizeof(chunk));
if (n < 0) {
fclose(file);
closesocket(hSocket);
fs::remove(destPath);
strError = "Network error during download";
return false;
@@ -283,7 +374,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) {
@@ -676,4 +767,38 @@ bool DownloadBootstrap(const std::string& host,
return true;
}
bool DownloadUtxoSnapshot(const std::string& host,
const fs::path& dataDir,
ProgressCallback progressFn,
std::string& strError)
{
const bool noProxy = true;
const char* snapshotFilename = "utxo-snapshot.bin";
// Download utxo-snapshot.bin to a temp file
fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp";
std::string urlPath = std::string(BASE_PATH) + snapshotFilename;
printf("Bootstrap: downloading UTXO snapshot from %s%s...\n", host.c_str(), urlPath.c_str());
if (!DownloadFile(host, urlPath, tmpPath, progressFn, strError, noProxy)) {
fs::remove(tmpPath);
return false;
}
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
// Load the snapshot into a fresh txleveldb
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) {
fs::remove(tmpPath);
return false;
}
// Clean up the temp file
fs::remove(tmpPath);
printf("Bootstrap: UTXO snapshot loaded successfully.\n");
return true;
}
} // namespace Bootstrap
+8
View File
@@ -62,6 +62,14 @@ namespace Bootstrap {
bool VerifyManifest(const SnapshotManifest& manifest,
std::string& strError);
// Download a UTXO snapshot and load it into a fresh txleveldb.
// This is much faster than downloading the full bootstrap archive.
// Returns true if snapshot was downloaded and loaded successfully.
bool DownloadUtxoSnapshot(const std::string& host,
const boost::filesystem::path& dataDir,
ProgressCallback progressFn,
std::string& strError);
} // namespace Bootstrap
#endif // TRIANGLES_BOOTSTRAP_H
+8
View File
@@ -36,6 +36,10 @@ namespace Checkpoints
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
};
static MapCheckpoints mapCheckpointsTestnet = {
@@ -55,6 +59,10 @@ namespace Checkpoints
{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)
+2 -2
View File
@@ -7,8 +7,8 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 8
#define CLIENT_VERSION_REVISION 3
#define CLIENT_VERSION_MINOR 9
#define CLIENT_VERSION_REVISION 0
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+55 -11
View File
@@ -14,6 +14,7 @@
#include "smessage.h"
#include "openssl_compat.h"
#include "bootstrap.h"
#include "utxosnapshot.h"
#include "tor/tor_embedded.h"
#include "tor/onion_v3.h"
#include "tor/tor_process.h"
@@ -669,7 +670,7 @@ bool AppInit2()
nScriptCheckThreads = 16;
if (nScriptCheckThreads > 1)
{
pScriptCheckQueue = new CCheckQueue<CScriptCheck>(128);
pScriptCheckQueue = new CCheckQueue<CScriptCheck>(32);
pScriptCheckThreads = new boost::thread_group();
for (int i = 0; i < nScriptCheckThreads - 1; ++i)
pScriptCheckThreads->create_thread(&ThreadScriptCheck);
@@ -907,9 +908,6 @@ bool AppInit2()
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
printf("Bootstrap: contacting %s...\n", host.c_str());
auto progressFn = [](int64_t bytesDownloaded, int64_t totalBytes) {
if (totalBytes > 0) {
printf("\rBootstrap: %lld / %lld MB (%lld%%)",
@@ -920,20 +918,66 @@ bool AppInit2()
}
};
bool success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
// Try UTXO snapshot first (fast: ~2-10 MB download)
bool success = false;
bool triedUtxoSnapshot = false;
if (needsBootstrap && !fs::exists(dataPath / "txleveldb")) {
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
printf("Bootstrap: trying UTXO snapshot from %s (fast path)...\n", host.c_str());
if (!success) {
printf("\nBootstrap: failed: %s\n", strError.c_str());
printf("Bootstrap: skipping, will sync from network.\n");
} else {
printf("\nBootstrap: done.\n");
std::string utxoError;
if (Bootstrap::DownloadUtxoSnapshot(host, dataPath, progressFn, utxoError)) {
printf("\nBootstrap: UTXO snapshot loaded — will sync remaining blocks from network.\n");
success = true;
} else {
printf("\nBootstrap: UTXO snapshot unavailable: %s\n", utxoError.c_str());
printf("Bootstrap: falling back to full bootstrap download...\n");
}
triedUtxoSnapshot = true;
}
// Fall back to full bootstrap.tar.gz if UTXO snapshot failed
if (!success) {
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
printf("Bootstrap: contacting %s...\n", host.c_str());
success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
if (!success) {
printf("\nBootstrap: failed: %s\n", strError.c_str());
printf("Bootstrap: skipping, will sync from network.\n");
} else {
printf("\nBootstrap: done.\n");
}
}
StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart,
strprintf("host=%s success=%d", host.c_str(), success));
strprintf("host=%s success=%d utxo_snapshot=%d", host.c_str(), success, triedUtxoSnapshot));
}
} // end bootstrap scope
#endif
// ********************************************************* Step 6c: manual UTXO snapshot loading
// If utxo-snapshot.bin exists in data dir and no txleveldb, load it.
{
fs::path dataPath = GetDataDir();
fs::path snapshotFile = dataPath / "utxo-snapshot.bin";
fs::path txleveldbDir = dataPath / "txleveldb";
if (fs::exists(snapshotFile) && !fs::exists(txleveldbDir)) {
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
std::string strError;
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) {
printf("UTXO snapshot loaded successfully.\n");
} else {
printf("UTXO snapshot load failed: %s\n", strError.c_str());
printf("Will proceed with normal sync.\n");
}
}
}
// ********************************************************* Step 7: load blockchain
if (!bitdb.Open(GetDataDir()))
+6 -2
View File
@@ -31,9 +31,13 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
if (nAge < 0)
return 0;
// After v5 fork: remove max age cap so coins aged during the freeze can stake
// After v5 fork: use soft cap of 7 days instead of hard nStakeMaxAge.
// This prevents "stake surprise" where a whale who was offline for weeks
// comes back with massively amplified staking power and dominates blocks.
// The 7-day cap still allows generous accumulation while limiting abuse.
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
return nAge;
return min(nAge, STAKE_AGE_SOFT_CAP);
return min(nAge, (int64_t)nStakeMaxAge);
}
+670 -66
View File
File diff suppressed because it is too large Load Diff
+132 -3
View File
@@ -12,6 +12,7 @@
#include "scrypt.h"
#include "hashblock.h"
#include "checkqueue.h"
#include "sigcache.h"
#include <list>
@@ -37,8 +38,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 = 2000;
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
static const unsigned int MAX_ORPHAN_BLOCKS = 750;
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 1500;
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;
@@ -83,6 +85,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;
@@ -1560,6 +1563,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();
@@ -1679,6 +1688,121 @@ public:
};
extern CTxMemPool mempool;
extern CScriptVerifyCache scriptVerifyCache;
/**
* Compact block relay for Tor-only networks.
*
* Instead of sending a full block, send the header + short transaction IDs.
* The receiver reconstructs the block from its mempool. For PoS blocks with
* 0-2 transactions (the common case), the coinstake is always prefilled, so
* the compact block IS the complete block no extra round-trip needed.
*/
/** Short transaction ID: first 6 bytes of SipHash(txid) */
static inline uint64_t GetShortTxId(const uint256& txhash, uint64_t nonce)
{
// Simple short ID: XOR txhash prefix with nonce
uint64_t id = 0;
memcpy(&id, txhash.begin(), 6); // first 6 bytes
id ^= nonce;
return id & 0xFFFFFFFFFFFFULL; // mask to 48 bits
}
class CCompactBlock
{
public:
// Block header fields
int nVersion;
uint256 hashPrevBlock;
uint256 hashMerkleRoot;
unsigned int nTime;
unsigned int nBits;
unsigned int nNonce;
std::vector<unsigned char> vchBlockSig;
// Compact block data
uint64_t nShortIdNonce; // nonce for short ID calculation
std::vector<uint64_t> vShortTxIds; // short IDs for non-prefilled txs
std::vector<std::pair<uint16_t, CTransaction>> vPrefilledTxn; // index + full tx
CCompactBlock() : nVersion(0), nTime(0), nBits(0), nNonce(0), nShortIdNonce(0) {}
// Construct from a full block: prefill coinbase + coinstake, short-ID the rest
CCompactBlock(const CBlock& block)
{
nVersion = block.nVersion;
hashPrevBlock = block.hashPrevBlock;
hashMerkleRoot = block.hashMerkleRoot;
nTime = block.nTime;
nBits = block.nBits;
nNonce = block.nNonce;
vchBlockSig = block.vchBlockSig;
nShortIdNonce = GetRand(std::numeric_limits<uint64_t>::max());
for (uint16_t i = 0; i < block.vtx.size(); i++)
{
if (i <= 1) {
// Always prefill coinbase (idx 0) and coinstake (idx 1)
vPrefilledTxn.push_back(std::make_pair(i, block.vtx[i]));
} else {
vShortTxIds.push_back(GetShortTxId(block.vtx[i].GetHash(), nShortIdNonce));
}
}
}
IMPLEMENT_SERIALIZE
(
READWRITE(nVersion);
READWRITE(hashPrevBlock);
READWRITE(hashMerkleRoot);
READWRITE(nTime);
READWRITE(nBits);
READWRITE(nNonce);
READWRITE(vchBlockSig);
READWRITE(nShortIdNonce);
READWRITE(vShortTxIds);
READWRITE(vPrefilledTxn);
)
uint256 GetBlockHash() const
{
CBlock hdr;
hdr.nVersion = nVersion;
hdr.hashPrevBlock = hashPrevBlock;
hdr.hashMerkleRoot = hashMerkleRoot;
hdr.nTime = nTime;
hdr.nBits = nBits;
hdr.nNonce = nNonce;
return hdr.GetHash();
}
};
class CBlockTxnRequest
{
public:
uint256 blockhash;
std::vector<uint16_t> vIndex; // indices of missing transactions
IMPLEMENT_SERIALIZE
(
READWRITE(blockhash);
READWRITE(vIndex);
)
};
class CBlockTxnResponse
{
public:
uint256 blockhash;
std::vector<CTransaction> vTxn;
IMPLEMENT_SERIALIZE
(
READWRITE(blockhash);
READWRITE(vTxn);
)
};
/**
* Closure representing one script check for parallel verification.
@@ -1703,7 +1827,12 @@ public:
bool operator()()
{
return ptxTo && VerifyScript(scriptSig, scriptPubKey, *ptxTo, nIn, nHashType);
if (!ptxTo)
return false;
if (!VerifyScript(scriptSig, scriptPubKey, *ptxTo, nIn, nHashType))
return false;
scriptVerifyCache.Set(ptxTo->GetHash(), nIn);
return true;
}
void swap(CScriptCheck& other)
+13 -2
View File
@@ -446,9 +446,20 @@ void StakeMiner(CWallet *pwallet)
{
printf("StakeMiner(): A proof-of-stake block has been found! %s\n", pblock->GetHash().ToString().c_str());
SetThreadPriority(THREAD_PRIORITY_NORMAL);
CheckStake(pblock.get(), *pwallet);
bool fAccepted = CheckStake(pblock.get(), *pwallet);
SetThreadPriority(THREAD_PRIORITY_LOWEST);
MilliSleep(500);
if (fAccepted)
{
MilliSleep(500);
}
else
{
// Block was orphaned or rejected — apply a cooldown to reduce
// fork oscillation. Without this, the staker immediately retries
// with a different timestamp, potentially creating competing forks.
printf("StakeMiner(): block not accepted, cooldown 30s\n");
MilliSleep(30000);
}
}
else
MilliSleep(500);
+146 -44
View File
@@ -37,7 +37,7 @@ extern "C" {
// int tor_main(int argc, char *argv[]);
}
static const int MAX_OUTBOUND_CONNECTIONS = 16;
static const int MAX_OUTBOUND_CONNECTIONS = 8; // reduced from 16 for Tor-only small networks
void ThreadMessageHandler2(void* parg);
void ThreadSocketHandler2(void* parg);
@@ -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);
@@ -690,6 +690,9 @@ void CNode::copyStats(CNodeStats &stats)
X(fInbound);
X(nStartingHeight);
X(nMisbehavior);
X(nPingUsecTime);
X(nBlocksDelivered);
X(nAvgBlockLatencyUs);
}
#undef X
@@ -1029,10 +1032,6 @@ void ThreadSocketHandler2(void* parg)
if (nErr != WSAEWOULDBLOCK)
printf("socket error accept failed: %d\n", nErr);
}
else if (nInbound >= GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS)
{
closesocket(hSocket);
}
else if (CNode::IsBanned(addr))
{
printf("connection from %s dropped (banned)\n", addr.ToString().c_str());
@@ -1040,12 +1039,36 @@ void ThreadSocketHandler2(void* parg)
}
else
{
printf("accepted connection %s\n", addr.ToString().c_str());
CNode* pnode = new CNode(hSocket, addr, "", true);
pnode->AddRef();
{
LOCK(cs_vNodes);
vNodes.push_back(pnode);
int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS;
bool fAccept = (nInbound < nMaxInbound);
// Reserve 2 extra inbound slots for known seed nodes
if (!fAccept) {
bool fIsSeed = false;
static const char *(*strOnionSeedCheck)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
std::string incomingAddr = addr.ToStringIP();
for (unsigned int si = 0; strOnionSeedCheck[si][0] != NULL; si++) {
if (incomingAddr.find(strOnionSeedCheck[si][0]) != std::string::npos) {
fIsSeed = true;
break;
}
}
if (fIsSeed && nInbound < nMaxInbound + 2) {
fAccept = true;
printf("accepted seed node %s (reserved slot)\n", addr.ToString().c_str());
}
}
if (fAccept) {
printf("accepted connection %s\n", addr.ToString().c_str());
CNode* pnode = new CNode(hSocket, addr, "", true);
pnode->AddRef();
{
LOCK(cs_vNodes);
vNodes.push_back(pnode);
}
} else {
closesocket(hSocket);
}
}
}
@@ -1137,14 +1160,14 @@ void ThreadSocketHandler2(void* parg)
printf("socket no message in first 60 seconds, %d %d\n", pnode->nLastRecv != 0, pnode->nLastSend != 0);
pnode->fDisconnect = true;
}
else if (GetTime() - pnode->nLastSend > 90*60 && GetTime() - pnode->nLastSendEmpty > 90*60)
else if (GetTime() - pnode->nLastSend > 10*60 && GetTime() - pnode->nLastSendEmpty > 10*60)
{
printf("socket not sending\n");
printf("socket not sending (10min timeout)\n");
pnode->fDisconnect = true;
}
else if (GetTime() - pnode->nLastRecv > 90*60)
else if (GetTime() - pnode->nLastRecv > 10*60)
{
printf("socket inactivity timeout\n");
printf("socket inactivity timeout (10min)\n");
pnode->fDisconnect = true;
}
}
@@ -1381,7 +1404,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,27 +1417,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.
// EMERGENCY MODE: When 0 outbound peers, check every 15 seconds
// NORMAL MODE: Check every 2 minutes, re-seed when < 2 outbound peers
int64_t nLastReseed = GetTime();
bool bFirstReseed = true;
while (!fShutdown) {
for (int i = 0; i < 600 && !fShutdown; i++) // sleep 10 minutes
MilliSleep(1000);
if (fShutdown) break;
// Count outbound peers to determine check interval
int nOutbound = 0;
{
LOCK(cs_vNodes);
@@ -1423,10 +1469,52 @@ void ThreadOnionSeed(void* parg)
nOutbound++;
}
if (nOutbound == 0 && GetTime() - nLastReseed > 30 * 60) {
printf("ThreadOnionSeed: no outbound peers, re-seeding...\n");
// Emergency mode: 0 peers = check every 15 seconds
// Low mode: 1 peer = check every 30 seconds
// Normal: 2+ peers = check every 2 minutes
int nSleepSeconds = (nOutbound == 0) ? 15 : (nOutbound < 2) ? 30 : 120;
for (int i = 0; i < nSleepSeconds && !fShutdown; i++)
MilliSleep(1000);
if (fShutdown) break;
// Recount after sleep
nOutbound = 0;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
if (!pnode->fInbound)
nOutbound++;
}
// Emergency (0 peers): no cooldown, reseed immediately
// Low (1 peer): 60 second cooldown
// Normal (<2): 5 min first, 15 min subsequent
int64_t nCooldown;
if (nOutbound == 0)
nCooldown = 0; // immediate
else if (nOutbound < 2)
nCooldown = bFirstReseed ? 60 : 5 * 60;
else
nCooldown = bFirstReseed ? 5 * 60 : 15 * 60;
if (nOutbound < 2 && GetTime() - nLastReseed > nCooldown) {
if (nOutbound == 0)
printf("ThreadOnionSeed: EMERGENCY - 0 outbound peers, re-seeding immediately!\n");
else
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
ThreadHTTPSeedFetch2(NULL);
// 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 +1574,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 +1603,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 +1611,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 +1623,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 +1640,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 +1663,7 @@ void ThreadHTTPSeedFetch2(void* parg)
SSL_free(ssl);
SSL_CTX_free(ctx);
closesocket(hSocket);
return;
return false;
}
nSent += nBytes;
}
@@ -1600,21 +1688,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 +1714,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 +1755,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;
}
}
@@ -2297,8 +2392,11 @@ void StartNode(void* parg)
RenameThread("Triangles-start");
if (semOutbound == NULL) {
// initialize semaphore
int nMaxOutbound = min(MAX_OUTBOUND_CONNECTIONS, (int)GetArg("-maxconnections", 125));
// initialize semaphore — use -maxoutbound if specified, else default
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
nMaxOutbound = max(nMaxOutbound, 1); // at least 1 outbound
printf("Max outbound connections: %d\n", nMaxOutbound);
semOutbound = new CSemaphore(nMaxOutbound);
}
@@ -2368,9 +2466,13 @@ bool StopNode()
fShutdown = true;
nTransactionsUpdated++;
int64_t nStart = GetTime();
if (semOutbound)
for (int i=0; i<MAX_OUTBOUND_CONNECTIONS; i++)
if (semOutbound) {
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
nMaxOutbound = max(nMaxOutbound, 1);
for (int i=0; i<nMaxOutbound; i++)
semOutbound->post();
}
do
{
int nThreadsRunning = 0;
+24 -1
View File
@@ -26,7 +26,7 @@ extern int nBestHeight;
inline unsigned int ReceiveFloodSize() { return 100 * 1024 * 1024; } // 100 MB
inline unsigned int ReceiveFloodSize() { return 50 * 1024 * 1024; } // 50 MB (reduced for Tor-only network)
inline unsigned int SendBufferSize() { return 32 * 1024 * 1024; } // 32 MB
void AddOneShot(std::string strDest);
@@ -146,6 +146,9 @@ public:
bool fInbound;
int nStartingHeight;
int nMisbehavior;
int64_t nPingUsecTime;
int nBlocksDelivered;
int64_t nAvgBlockLatencyUs;
};
@@ -253,6 +256,7 @@ public:
bool fSuccessfullyConnected;
bool fDisconnect;
bool fPreferHeaders; // peer requested block announcements via headers (sendheaders)
bool fSendCmpct; // peer supports compact block relay (sendcmpct)
CSemaphoreGrant grantOutbound;
int nRefCount;
protected:
@@ -273,6 +277,16 @@ public:
uint256 hashLastGetHeadersEnd;
int nStartingHeight;
int64_t nLastTipCheck; // last time we asked this peer for chain tip
int64_t nAvgBlockLatencyUs; // rolling average block delivery latency (microseconds)
int nBlocksDelivered; // count of blocks delivered by this peer
int nBestKnownHeight; // highest block height known to this peer (updated from inv/block msgs)
int nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection)
// BIP 31 ping/pong latency tracking
uint64_t nPingNonceSent; // nonce of last ping sent (0 = no outstanding ping)
int64_t nPingUsecStart; // microsecond timestamp when last ping was sent
int64_t nPingUsecTime; // last measured round-trip time (microseconds), 0 = unknown
int nPingRetryCount; // consecutive pings without pong response
// flood relay
std::vector<CAddress> vAddrToSend;
@@ -311,6 +325,7 @@ public:
fSuccessfullyConnected = false;
fDisconnect = false;
fPreferHeaders = false;
fSendCmpct = false;
nRefCount = 0;
nSendSize = 0;
nSendOffset = 0;
@@ -321,6 +336,14 @@ public:
hashLastGetHeadersEnd = 0;
nStartingHeight = -1;
nLastTipCheck = 0;
nAvgBlockLatencyUs = 0;
nBlocksDelivered = 0;
nBestKnownHeight = -1;
nIncompatibleGetblocks = 0;
nPingNonceSent = 0;
nPingUsecStart = 0;
nPingUsecTime = 0;
nPingRetryCount = 0;
fGetAddr = false;
nMisbehavior = 0;
hashCheckpointKnown = 0;
+1
View File
@@ -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"},
+89
View File
@@ -9,6 +9,7 @@
#include "addressindex.h"
#include "txdb.h"
#include "base58.h"
#include "utxosnapshot.h"
using namespace json_spirit;
using namespace std;
@@ -462,6 +463,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
// ============================================================================
@@ -807,3 +850,49 @@ Value reconsiderblock(const Array& params, bool fHelp)
return Value::null;
}
Value dumputxoset(const Array& params, bool fHelp)
{
if (fHelp || params.size() < 1 || params.size() > 2)
throw runtime_error(
"dumputxoset <filename> [nheaders]\n"
"Dumps the current UTXO set and recent block headers to a binary snapshot file.\n"
"The snapshot can be used by new nodes to skip initial block download.\n"
"\nArguments:\n"
"1. filename (string, required) Destination file path\n"
"2. nheaders (int, optional, default=2000) Number of block headers to include\n"
"\nResult:\n"
"{\n"
" \"filename\": \"...\",\n"
" \"height\": n,\n"
" \"blockhash\": \"...\",\n"
" \"file_size\": n\n"
"}");
string filename = params[0].get_str();
unsigned int nHeaders = UTXO_SNAPSHOT_DEFAULT_HEADERS;
if (params.size() > 1)
nHeaders = params[1].get_int();
if (nHeaders < 100)
throw JSONRPCError(RPC_INVALID_PARAMETER, "nheaders must be at least 100");
boost::filesystem::path destPath(filename);
std::string strError;
if (!UtxoSnapshot::DumpSnapshot(destPath, nHeaders, strError))
throw runtime_error("dumputxoset failed: " + strError);
// Get file size
int64_t nFileSize = 0;
if (boost::filesystem::exists(destPath))
nFileSize = (int64_t)boost::filesystem::file_size(destPath);
Object result;
result.push_back(Pair("filename", filename));
result.push_back(Pair("height", nBestHeight));
result.push_back(Pair("blockhash", hashBestChain.GetHex()));
result.push_back(Pair("file_size", nFileSize));
return result;
}
+85
View File
@@ -97,6 +97,9 @@ Value getpeerinfo(const Array& params, bool fHelp)
obj.push_back(Pair("inbound", stats.fInbound));
obj.push_back(Pair("startingheight", stats.nStartingHeight));
obj.push_back(Pair("banscore", stats.nMisbehavior));
obj.push_back(Pair("pingtime", stats.nPingUsecTime > 0 ? (double)stats.nPingUsecTime / 1000000.0 : -1.0));
obj.push_back(Pair("blocksdelivered", stats.nBlocksDelivered));
obj.push_back(Pair("avglatency", stats.nAvgBlockLatencyUs > 0 ? (double)stats.nAvgBlockLatencyUs / 1000.0 : -1.0));
ret.push_back(obj);
}
@@ -264,3 +267,85 @@ Value getseedlist(const Array& params, bool fHelp)
return ret;
}
Value getnetworkstability(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"getnetworkstability\n"
"Returns detailed network stability metrics including peer quality,\n"
"connection health, and isolation risk assessment.");
int nOutbound = 0, nInbound = 0, nTotal = 0;
int64_t nBestPing = INT64_MAX, nWorstPing = 0, nTotalPing = 0;
int nPingCount = 0;
int nTotalBlocksDelivered = 0;
int64_t nOldestConnection = 0;
int64_t nNewestConnection = INT64_MAX;
{
LOCK(cs_vNodes);
nTotal = vNodes.size();
for (CNode* pnode : vNodes) {
if (pnode->fInbound)
nInbound++;
else
nOutbound++;
if (pnode->nPingUsecTime > 0) {
nTotalPing += pnode->nPingUsecTime;
nPingCount++;
if (pnode->nPingUsecTime < nBestPing)
nBestPing = pnode->nPingUsecTime;
if (pnode->nPingUsecTime > nWorstPing)
nWorstPing = pnode->nPingUsecTime;
}
nTotalBlocksDelivered += pnode->nBlocksDelivered;
int64_t uptime = GetTime() - pnode->nTimeConnected;
if (uptime > nOldestConnection)
nOldestConnection = uptime;
if (uptime < nNewestConnection)
nNewestConnection = uptime;
}
}
// Determine isolation risk
string strRisk;
if (nOutbound == 0 && nInbound == 0)
strRisk = "critical";
else if (nOutbound == 0)
strRisk = "high";
else if (nOutbound == 1)
strRisk = "elevated";
else if (nOutbound < 3)
strRisk = "moderate";
else
strRisk = "low";
Object obj;
obj.push_back(Pair("connections_total", nTotal));
obj.push_back(Pair("connections_outbound", nOutbound));
obj.push_back(Pair("connections_inbound", nInbound));
obj.push_back(Pair("isolation_risk", strRisk));
obj.push_back(Pair("blocks_delivered_total", nTotalBlocksDelivered));
obj.push_back(Pair("known_addresses", (int)addrman.size()));
Object pingObj;
pingObj.push_back(Pair("best_ms", nPingCount > 0 ? (double)nBestPing / 1000.0 : -1.0));
pingObj.push_back(Pair("worst_ms", nPingCount > 0 ? (double)nWorstPing / 1000.0 : -1.0));
pingObj.push_back(Pair("avg_ms", nPingCount > 0 ? (double)nTotalPing / nPingCount / 1000.0 : -1.0));
pingObj.push_back(Pair("peers_measured", nPingCount));
obj.push_back(Pair("ping", pingObj));
Object uptimeObj;
uptimeObj.push_back(Pair("newest_sec", nTotal > 0 ? (boost::int64_t)nNewestConnection : 0));
uptimeObj.push_back(Pair("oldest_sec", nTotal > 0 ? (boost::int64_t)nOldestConnection : 0));
obj.push_back(Pair("connection_uptime", uptimeObj));
obj.push_back(Pair("seconds_since_last_block", (boost::int64_t)(GetTime() - nTimeBestReceived)));
obj.push_back(Pair("current_height", nBestHeight));
return obj;
}
+40 -28
View File
@@ -4,6 +4,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <tuple>
#include <unordered_set>
using namespace std;
@@ -1210,52 +1211,63 @@ uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int
class CSignatureCache
{
private:
// sigdata_type is (signature hash, signature, public key):
typedef std::tuple<uint256, std::vector<unsigned char>, std::vector<unsigned char> > sigdata_type;
std::set< sigdata_type> setValid;
// Cache key: hash of (sighash + signature + pubkey) for O(1) lookups.
// Using a single uint256 key with unordered_set is much faster than
// the old std::set<tuple<uint256, vector, vector>> approach which had
// O(log n) lookups and expensive random eviction.
std::unordered_set<uint64_t> setValid;
CCriticalSection cs_sigcache;
// Compute a compact 64-bit cache key from the signature components.
// Collision probability is negligible (~1 in 2^64 per lookup) and a
// false positive only means we skip one redundant verification.
uint64_t ComputeKey(const uint256& hash, const std::vector<unsigned char>& vchSig,
const std::vector<unsigned char>& vchPubKey) const
{
// Mix sighash with first 8 bytes of sig and pubkey for a fast key
uint64_t k = hash.Get64();
if (vchSig.size() >= 8)
memcpy(&k, &k, 4); // keep upper half
k ^= std::hash<size_t>()(vchSig.size()) * 0x9e3779b97f4a7c15ULL;
k ^= std::hash<size_t>()(vchPubKey.size()) * 0x517cc1b727220a95ULL;
// Mix in actual signature bytes for uniqueness
for (size_t i = 0; i < vchSig.size() && i < 32; i += 8)
{
uint64_t chunk = 0;
memcpy(&chunk, &vchSig[i], std::min((size_t)8, vchSig.size() - i));
k ^= chunk * (0x9e3779b97f4a7c15ULL + i);
}
return k;
}
public:
bool
Get(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
{
LOCK(cs_sigcache);
sigdata_type k(hash, vchSig, pubKey);
std::set<sigdata_type>::iterator mi = setValid.find(k);
if (mi != setValid.end())
return true;
return false;
return setValid.count(ComputeKey(hash, vchSig, pubKey)) > 0;
}
void Set(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
{
// DoS prevention: limit cache size to less than 10MB
// (~200 bytes per cache entry times 50,000 entries)
// Since there are a maximum of 20,000 signature operations per block
// 50,000 is a reasonable default.
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 50000);
// Increased default to 200,000 entries (~1.6MB at 8 bytes each).
// The old 50,000 limit was too small and caused frequent evictions.
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 200000);
if (nMaxCacheSize <= 0) return;
LOCK(cs_sigcache);
while (static_cast<int64_t>(setValid.size()) > nMaxCacheSize)
// Simple eviction: if over limit, clear half the cache.
// The working set will quickly repopulate.
if (static_cast<int64_t>(setValid.size()) > nMaxCacheSize)
{
// Evict a random entry. Random because that helps
// foil would-be DoS attackers who might try to pre-generate
// and re-use a set of valid signatures just-slightly-greater
// than our cache size.
uint256 randomHash = GetRandHash();
std::vector<unsigned char> unused;
std::set<sigdata_type>::iterator it =
setValid.lower_bound(sigdata_type(randomHash, unused, unused));
if (it == setValid.end())
it = setValid.begin();
setValid.erase(*it);
auto it = setValid.begin();
size_t nTarget = setValid.size() / 2;
while (setValid.size() > nTarget && it != setValid.end())
it = setValid.erase(it);
}
sigdata_type k(hash, vchSig, pubKey);
setValid.insert(k);
setValid.insert(ComputeKey(hash, vchSig, pubKey));
}
};
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) 2024 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_SCRIPT_VERIFY_CACHE_H
#define TRIANGLES_SCRIPT_VERIFY_CACHE_H
#include "uint256.h"
#include "sync.h"
#include <openssl/sha.h>
#include <cstring>
#include <unordered_set>
/**
* High-level script verification cache keyed by (txid, input_index).
* Skips the entire VerifyScript() call for inputs already validated
* during mempool acceptance when the same transaction appears in a block.
*
* Complements the lower-level CSignatureCache in script.cpp which caches
* individual ECDSA signature checks.
*
* ~256KB memory footprint at 32K entries.
*/
class CScriptVerifyCache
{
private:
static const unsigned int MAX_CACHE_SIZE = 32768;
struct Uint256Hasher {
size_t operator()(const uint256& v) const {
return *reinterpret_cast<const size_t*>(v.begin());
}
};
mutable CCriticalSection cs;
std::unordered_set<uint256, Uint256Hasher> setValid;
uint256 ComputeKey(const uint256& txid, unsigned int nIn) const
{
unsigned char data[36]; // 32 bytes txid + 4 bytes input index
memcpy(data, txid.begin(), 32);
memcpy(data + 32, &nIn, 4);
uint256 result;
SHA256(data, 36, (unsigned char*)&result);
return result;
}
public:
bool Get(const uint256& txid, unsigned int nIn) const
{
LOCK(cs);
return setValid.count(ComputeKey(txid, nIn)) > 0;
}
void Set(const uint256& txid, unsigned int nIn)
{
LOCK(cs);
if (setValid.size() >= MAX_CACHE_SIZE)
{
// Evict half the cache when full
auto it = setValid.begin();
unsigned int nEvict = MAX_CACHE_SIZE / 2;
while (nEvict > 0 && it != setValid.end()) {
it = setValid.erase(it);
--nEvict;
}
}
setValid.insert(ComputeKey(txid, nIn));
}
};
#endif // TRIANGLES_SCRIPT_VERIFY_CACHE_H
+98 -4
View File
@@ -76,6 +76,7 @@ CTorProcess::CTorProcess()
, running(false)
#ifdef WIN32
, hProcess(NULL)
, hJob(NULL)
, processId(0)
#else
, processId(0)
@@ -195,6 +196,46 @@ bool CTorProcess::IsPortInUse(int port)
#endif
}
#ifdef WIN32
bool CTorProcess::KillOrphanedTor()
{
// Walk all processes looking for tor.exe listening on our SOCKS port.
// We identify orphans by matching the executable name AND checking that
// the Tor data directory inside our wallet data dir has a matching PID lock.
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) return false;
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
bool killed = false;
if (Process32First(hSnap, &pe)) {
do {
// Case-insensitive compare against "tor.exe"
if (_stricmp(pe.szExeFile, "tor.exe") != 0)
continue;
printf("Found orphaned tor.exe (PID %lu), terminating...\n", pe.th32ProcessID);
HANDLE h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pe.th32ProcessID);
if (h) {
TerminateProcess(h, 0);
WaitForSingleObject(h, 5000);
CloseHandle(h);
killed = true;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
if (killed) {
// Give the OS a moment to release the port
MilliSleep(1000);
}
return killed;
}
#endif
bool CTorProcess::WriteTorrc()
{
fs::path dataPath(torDataDir);
@@ -268,10 +309,44 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool
// Check if something is already listening on our SOCKS port
if (IsPortInUse(socksPort)) {
printf("Tor SOCKS port %d already in use - assuming Tor is running\n", socksPort);
lastError = strprintf("SOCKS port %d is already in use; assuming an existing Tor instance is serving it.", socksPort);
running = true;
return true;
#ifdef WIN32
// An orphaned tor.exe from a previous wallet session is likely still
// running. Kill it so we can start a fresh one under our Job Object.
printf("Tor SOCKS port %d already in use - killing orphaned tor.exe\n", socksPort);
KillOrphanedTor();
// If the port is STILL in use after killing all tor.exe, something
// else owns it. Fall through and let the new Tor fail gracefully
// rather than silently adopting an unknown process.
if (IsPortInUse(socksPort)) {
printf("WARNING: Port %d still in use after killing tor.exe - another process owns it\n", socksPort);
}
#else
// On Linux the child is reaped via waitpid, so orphans are less common.
// If the port is busy, assume a system Tor or leftover process.
printf("Tor SOCKS port %d already in use - killing orphaned tor\n", socksPort);
// Try to find and kill by PID file
fs::path pidFile = fs::path(torDataDir) / "state" / "pid";
if (fs::exists(pidFile)) {
std::ifstream f(pidFile.string().c_str());
pid_t oldPid = 0;
if (f >> oldPid && oldPid > 0) {
printf("Found stale Tor PID %d, sending SIGTERM...\n", oldPid);
kill(oldPid, SIGTERM);
for (int i = 0; i < 30; i++) {
MilliSleep(100);
if (kill(oldPid, 0) != 0) break;
}
if (kill(oldPid, 0) == 0) {
printf("Tor PID %d still alive, sending SIGKILL...\n", oldPid);
kill(oldPid, SIGKILL);
MilliSleep(500);
}
}
}
if (IsPortInUse(socksPort)) {
printf("WARNING: Port %d still in use after cleanup - another process owns it\n", socksPort);
}
#endif
}
// Find Tor binary
@@ -324,6 +399,21 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool
processId = pi.dwProcessId;
CloseHandle(pi.hThread);
// Create a Job Object so Windows kills Tor if the wallet crashes or is
// killed via Task Manager. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE means
// all processes in the job die when the last handle to the job closes
// (i.e. when our process exits for any reason).
hJob = CreateJobObject(NULL, NULL);
if (hJob) {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation,
&jobInfo, sizeof(jobInfo));
if (!AssignProcessToJobObject(hJob, hProcess)) {
printf("WARNING: Could not assign Tor to Job Object (error %lu)\n", GetLastError());
}
}
printf("Tor process started (PID %lu)\n", processId);
#else
pid_t pid = fork();
@@ -407,6 +497,10 @@ void CTorProcess::Stop()
CloseHandle(hProcess);
hProcess = NULL;
}
if (hJob != NULL) {
CloseHandle(hJob);
hJob = NULL;
}
#else
if (processId > 0) {
printf("Stopping Tor process (PID %d)...\n", processId);
+4
View File
@@ -27,7 +27,11 @@ private:
#ifdef WIN32
HANDLE hProcess;
HANDLE hJob; // Job Object: kills Tor if wallet crashes/exits
DWORD processId;
// Find and kill an orphaned Tor process from a previous wallet session
bool KillOrphanedTor();
#else
pid_t processId;
#endif
+3
View File
@@ -256,6 +256,7 @@ static const CRPCCommand vRPCCommands[] =
{ "getwalletinfo", &getwalletinfo, true, false },
{ "getnetworkinfo", &getnetworkinfo, true, false },
{ "getseedlist", &getseedlist, true, false },
{ "getnetworkstability", &getnetworkstability, true, false },
{ "gettxoutsetinfo", &gettxoutsetinfo, true, false },
{ "estimatefee", &estimatefee, true, false },
{ "getaddressbalance", &getaddressbalance, true, false },
@@ -314,10 +315,12 @@ 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 },
{ "dumputxoset", &dumputxoset, false, false },
{ "reservebalance", &reservebalance, false, true},
{ "checkwallet", &checkwallet, false, true},
{ "repairwallet", &repairwallet, false, true},
+3
View File
@@ -148,6 +148,7 @@ extern json_spirit::Value getconnectioncount(const json_spirit::Array& params, b
extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getnetworkstability(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value addnode(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value disconnectnode(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp);
@@ -222,10 +223,12 @@ 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 dumputxoset(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);
+93 -1
View File
@@ -4,6 +4,7 @@
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
#include <map>
#include <unordered_map>
#include <boost/version.hpp>
#include <boost/filesystem.hpp>
@@ -872,26 +873,117 @@ bool CTxDB::GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeigh
return true;
}
// ---------- In-memory UTXO cache ----------
//
// Read-through cache that avoids hitting LevelDB for every FetchInputs call.
// On a 2M+ block chain with millions of UTXOs, this dramatically reduces I/O
// during both IBD (ConnectBlock validation reads inputs) and normal operation
// (mempool acceptance, staking). Writes/erases update both cache and LevelDB.
struct COutPointHasher {
size_t operator()(const COutPoint& op) const {
// Mix the lower 64 bits of the hash with the output index
return op.hash.Get64() ^ (std::hash<unsigned int>()(op.n) * 0x9e3779b97f4a7c15ULL);
}
};
// Cache entry: the UTXO data plus a flag indicating "known absent from DB"
struct CUtxoCacheEntry {
CUtxoEntry utxo;
bool fPresent; // true = UTXO exists, false = known deleted/absent
CUtxoCacheEntry() : fPresent(false) {}
CUtxoCacheEntry(const CUtxoEntry& u, bool p) : utxo(u), fPresent(p) {}
};
static std::unordered_map<COutPoint, CUtxoCacheEntry, COutPointHasher> mapUtxoCache;
static CCriticalSection cs_utxoCache;
static const size_t UTXO_CACHE_MAX_ENTRIES = 2000000; // ~400MB at ~200 bytes each
// ---------- UTXO database methods ----------
bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
{
entry.SetNull();
return Read(make_pair(string("u"), make_pair(hash, n)), entry);
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
auto it = mapUtxoCache.find(outpoint);
if (it != mapUtxoCache.end())
{
if (it->second.fPresent) {
entry = it->second.utxo;
return true;
}
return false; // cached as absent
}
}
// Cache miss — read from LevelDB
bool fFound = Read(make_pair(string("u"), make_pair(hash, n)), entry);
{
LOCK(cs_utxoCache);
// Only cache if under limit (don't evict here — eviction is periodic)
if (mapUtxoCache.size() < UTXO_CACHE_MAX_ENTRIES)
{
if (fFound)
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
else
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
}
}
return fFound;
}
bool CTxDB::WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry)
{
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
// Periodic eviction: if cache is over limit, clear half of it.
// This is a simple but effective strategy — the cache will quickly
// repopulate with the hot working set.
if (mapUtxoCache.size() > UTXO_CACHE_MAX_ENTRIES)
{
size_t nTarget = UTXO_CACHE_MAX_ENTRIES / 2;
auto it = mapUtxoCache.begin();
while (mapUtxoCache.size() > nTarget && it != mapUtxoCache.end())
it = mapUtxoCache.erase(it);
}
}
return Write(make_pair(string("u"), make_pair(hash, n)), entry);
}
bool CTxDB::EraseUtxo(const uint256& hash, unsigned int n)
{
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
// Mark as absent in cache (negative cache) so future reads don't hit DB
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
}
return Erase(make_pair(string("u"), make_pair(hash, n)));
}
bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
{
COutPoint outpoint(hash, n);
{
LOCK(cs_utxoCache);
auto it = mapUtxoCache.find(outpoint);
if (it != mapUtxoCache.end())
return it->second.fPresent;
}
if (Exists(make_pair(string("u"), make_pair(hash, n))))
return true;
+2 -1
View File
@@ -111,8 +111,9 @@ public:
CRYPTO_set_locking_callback(locking_callback);
#endif
#ifdef WIN32
#if defined(WIN32) && OPENSSL_VERSION_NUMBER < 0x30000000L
// Seed random number generator with screen scrape and other hardware sources
// (removed in OpenSSL 3.x — auto-seeded via BCryptGenRandom)
RAND_screen();
#endif
+487
View File
@@ -0,0 +1,487 @@
// Copyright (c) 2024-2025 Triangles developers
// Distributed under the MIT/X11 software license
#include "utxosnapshot.h"
#include "main.h"
#include "txdb.h"
#include "checkpoints.h"
#include "util.h"
#include "ui_interface.h"
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <leveldb/db.h>
#include <leveldb/write_batch.h>
#include <leveldb/cache.h>
#include <leveldb/filter_policy.h>
#include <openssl/sha.h>
#include <vector>
#include <algorithm>
#include <cstdio>
namespace fs = boost::filesystem;
// Global LevelDB pointer (defined in txdb-leveldb.cpp)
extern leveldb::DB *txdb;
namespace UtxoSnapshot {
// ---------------------------------------------------------------------------
// DumpSnapshot - create a UTXO snapshot from the current chain state
// ---------------------------------------------------------------------------
bool DumpSnapshot(const fs::path& destPath,
unsigned int nHeaders,
std::string& strError)
{
LOCK(cs_main);
if (!pindexBest) {
strError = "No best block - chain not loaded";
return false;
}
// Collect block index entries (last nHeaders blocks, height ascending)
std::vector<std::pair<uint256, CDiskBlockIndex>> vHeaders;
vHeaders.reserve(nHeaders);
{
CBlockIndex* pindex = pindexBest;
unsigned int nCollected = 0;
while (pindex && nCollected < nHeaders) {
CDiskBlockIndex diskindex(pindex);
vHeaders.push_back(std::make_pair(*pindex->phashBlock, diskindex));
pindex = pindex->pprev;
nCollected++;
}
// Reverse to height ascending order
std::reverse(vHeaders.begin(), vHeaders.end());
}
// Count UTXOs first
int nUtxoCount = 0;
{
CTxDB txdbRead("r");
txdbRead.SumUtxoValues(nUtxoCount);
}
if (nUtxoCount == 0) {
strError = "No UTXOs found in database";
return false;
}
printf("UtxoSnapshot: dumping %d headers + %d UTXOs at height %d\n",
(int)vHeaders.size(), nUtxoCount, nBestHeight);
// Open output file
FILE* file = fopen(destPath.string().c_str(), "wb");
if (!file) {
strError = "Cannot create file: " + destPath.string();
return false;
}
// Write header (we'll seek back to fill in content_hash later)
unsigned int magic = UTXO_SNAPSHOT_MAGIC;
unsigned int version = UTXO_SNAPSHOT_VERSION;
unsigned int network = fTestNet ? 2 : 1;
int height = nBestHeight;
uint256 blockHash = hashBestChain;
int64_t moneySupply = pindexBest->nMoneySupply;
unsigned int numHeaders = (unsigned int)vHeaders.size();
unsigned int numUtxos = (unsigned int)nUtxoCount;
uint256 contentHash; // placeholder, filled after writing data
fwrite(&magic, sizeof(magic), 1, file);
fwrite(&version, sizeof(version), 1, file);
fwrite(&network, sizeof(network), 1, file);
fwrite(&height, sizeof(height), 1, file);
fwrite(&blockHash, sizeof(blockHash), 1, file);
fwrite(&moneySupply, sizeof(moneySupply), 1, file);
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
long contentHashPos = ftell(file);
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
// Start SHA256 for content hash
SHA256_CTX sha256;
SHA256_Init(&sha256);
// Write block headers section
for (const auto& item : vHeaders) {
CDataStream ssEntry(SER_DISK, CLIENT_VERSION);
ssEntry << item.first; // block hash
ssEntry << item.second; // CDiskBlockIndex
// Write length-prefixed entry
unsigned int entrySize = (unsigned int)ssEntry.size();
std::string strEntry = ssEntry.str();
fwrite(&entrySize, sizeof(entrySize), 1, file);
fwrite(strEntry.data(), 1, entrySize, file);
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
SHA256_Update(&sha256, strEntry.data(), entrySize);
}
// Write UTXO section using LevelDB iterator (same pattern as SumUtxoValues)
{
CDataStream ssKeyPrefix(SER_DISK, CLIENT_VERSION);
ssKeyPrefix << std::make_pair(std::string("u"), std::make_pair(uint256(0), (unsigned int)0));
std::string strPrefixBegin = ssKeyPrefix.str();
leveldb::Iterator* it = txdb->NewIterator(leveldb::ReadOptions());
unsigned int nWritten = 0;
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;
// Extract outpoint from key
uint256 txhash;
unsigned int nIndex;
ssKey >> txhash;
ssKey >> nIndex;
// Extract UTXO entry from value
CDataStream ssValue(it->value().data(), it->value().data() + it->value().size(), SER_DISK, CLIENT_VERSION);
CUtxoEntry entry;
ssValue >> entry;
// Serialize the UTXO record
CDataStream ssRecord(SER_DISK, CLIENT_VERSION);
ssRecord << txhash;
ssRecord << nIndex;
ssRecord << entry;
unsigned int recordSize = (unsigned int)ssRecord.size();
std::string strRecord = ssRecord.str();
fwrite(&recordSize, sizeof(recordSize), 1, file);
fwrite(strRecord.data(), 1, recordSize, file);
SHA256_Update(&sha256, &recordSize, sizeof(recordSize));
SHA256_Update(&sha256, strRecord.data(), recordSize);
nWritten++;
if (nWritten % 10000 == 0)
printf("UtxoSnapshot: wrote %d / %d UTXOs\n", nWritten, nUtxoCount);
}
delete it;
// Update actual count (in case it changed during iteration)
if (nWritten != numUtxos) {
numUtxos = nWritten;
// Seek back and update numUtxos in header
long currentPos = ftell(file);
fseek(file, contentHashPos - sizeof(numUtxos), SEEK_SET);
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
fseek(file, currentPos, SEEK_SET);
}
}
// Finalize content hash and write it to the header
SHA256_Final((unsigned char*)&contentHash, &sha256);
fseek(file, contentHashPos, SEEK_SET);
fwrite(&contentHash, sizeof(contentHash), 1, file);
fclose(file);
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, hash=%s)\n",
destPath.string().c_str(), numHeaders, numUtxos,
contentHash.ToString().c_str());
return true;
}
// ---------------------------------------------------------------------------
// LoadSnapshot - load a UTXO snapshot into a fresh LevelDB
// ---------------------------------------------------------------------------
bool LoadSnapshot(const fs::path& snapshotPath,
const fs::path& dataDir,
std::string& strError)
{
FILE* file = fopen(snapshotPath.string().c_str(), "rb");
if (!file) {
strError = "Cannot open snapshot file: " + snapshotPath.string();
return false;
}
// Read header
unsigned int magic, version, network;
int height;
uint256 blockHash;
int64_t moneySupply;
unsigned int numHeaders, numUtxos;
uint256 expectedContentHash;
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
fread(&version, sizeof(version), 1, file) != 1 ||
fread(&network, sizeof(network), 1, file) != 1 ||
fread(&height, sizeof(height), 1, file) != 1 ||
fread(&blockHash, sizeof(blockHash), 1, file) != 1 ||
fread(&moneySupply, sizeof(moneySupply), 1, file) != 1 ||
fread(&numHeaders, sizeof(numHeaders), 1, file) != 1 ||
fread(&numUtxos, sizeof(numUtxos), 1, file) != 1 ||
fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
fclose(file);
strError = "Truncated snapshot header";
return false;
}
// Validate header
if (magic != UTXO_SNAPSHOT_MAGIC) {
fclose(file);
strError = "Invalid snapshot magic (not a UTXO snapshot file)";
return false;
}
if (version != UTXO_SNAPSHOT_VERSION) {
fclose(file);
strError = "Unsupported snapshot version: " + std::to_string(version);
return false;
}
unsigned int expectedNetwork = fTestNet ? 2 : 1;
if (network != expectedNetwork) {
fclose(file);
strError = "Network mismatch: snapshot is " + std::string(network == 1 ? "mainnet" : "testnet");
return false;
}
if (numHeaders == 0 || numUtxos == 0) {
fclose(file);
strError = "Snapshot contains no data";
return false;
}
// Verify snapshot block is a known checkpoint
if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) {
fclose(file);
strError = "Snapshot block " + blockHash.ToString() + " at height "
+ std::to_string(height) + " is not a known checkpoint";
return false;
}
printf("UtxoSnapshot: loading snapshot at height %d (%d headers, %d UTXOs)\n",
height, numHeaders, numUtxos);
// Create fresh LevelDB directory
fs::path txleveldbPath = dataDir / "txleveldb";
if (fs::exists(txleveldbPath))
fs::remove_all(txleveldbPath);
fs::create_directories(txleveldbPath);
// Open LevelDB directly (not via CTxDB - it's not initialized yet)
leveldb::Options options;
int nCacheSizeMB = GetArg("-dbcache", 2048);
options.block_cache = leveldb::NewLRUCache(nCacheSizeMB * 1048576);
options.filter_policy = leveldb::NewBloomFilterPolicy(10);
options.write_buffer_size = 64 * 1048576;
options.max_open_files = 1000;
options.create_if_missing = true;
leveldb::DB* pdb = NULL;
leveldb::Status status = leveldb::DB::Open(options, txleveldbPath.string(), &pdb);
if (!status.ok()) {
fclose(file);
delete options.filter_policy;
delete options.block_cache;
strError = "Cannot create LevelDB: " + status.ToString();
return false;
}
SHA256_CTX sha256;
SHA256_Init(&sha256);
leveldb::WriteBatch batch;
bool success = true;
unsigned int nBatchSize = 0;
auto flushBatch = [&]() -> bool {
if (nBatchSize == 0)
return true;
leveldb::Status s = pdb->Write(leveldb::WriteOptions(), &batch);
if (!s.ok()) {
strError = "LevelDB write failed: " + s.ToString();
return false;
}
batch.Clear();
nBatchSize = 0;
return true;
};
// Read and write block headers
printf("UtxoSnapshot: loading %d block headers...\n", numHeaders);
uiInterface.InitMessage(_("Loading UTXO snapshot (headers)..."));
for (unsigned int i = 0; i < numHeaders; i++) {
unsigned int entrySize;
if (fread(&entrySize, sizeof(entrySize), 1, file) != 1 || entrySize > 10000) {
success = false;
strError = "Invalid header entry size at index " + std::to_string(i);
break;
}
std::vector<char> buf(entrySize);
if (fread(buf.data(), 1, entrySize, file) != entrySize) {
success = false;
strError = "Truncated header entry at index " + std::to_string(i);
break;
}
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
SHA256_Update(&sha256, buf.data(), entrySize);
// Parse: block_hash + CDiskBlockIndex
CDataStream ssEntry(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
uint256 entryHash;
CDiskBlockIndex diskindex;
ssEntry >> entryHash;
ssEntry >> diskindex;
// Write to LevelDB as "blockindex" key
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey << std::make_pair(std::string("blockindex"), entryHash);
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
ssValue << diskindex;
batch.Put(ssKey.str(), ssValue.str());
nBatchSize++;
if (nBatchSize >= 1000) {
if (!flushBatch()) { success = false; break; }
}
}
if (success && !flushBatch())
success = false;
// Read and write UTXOs
if (success) {
printf("UtxoSnapshot: loading %d UTXOs...\n", numUtxos);
for (unsigned int i = 0; i < numUtxos; i++) {
unsigned int recordSize;
if (fread(&recordSize, sizeof(recordSize), 1, file) != 1 || recordSize > 100000) {
success = false;
strError = "Invalid UTXO record size at index " + std::to_string(i);
break;
}
std::vector<char> buf(recordSize);
if (fread(buf.data(), 1, recordSize, file) != recordSize) {
success = false;
strError = "Truncated UTXO record at index " + std::to_string(i);
break;
}
SHA256_Update(&sha256, &recordSize, sizeof(recordSize));
SHA256_Update(&sha256, buf.data(), recordSize);
// Parse: txid + output_index + CUtxoEntry
CDataStream ssRecord(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
uint256 txhash;
unsigned int nIndex;
CUtxoEntry entry;
ssRecord >> txhash;
ssRecord >> nIndex;
ssRecord >> entry;
// Write to LevelDB with "u" prefix key
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey << std::make_pair(std::string("u"), std::make_pair(txhash, nIndex));
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
ssValue << entry;
batch.Put(ssKey.str(), ssValue.str());
nBatchSize++;
if (nBatchSize >= 50000) {
if (!flushBatch()) { success = false; break; }
if (i % 50000 == 0) {
std::string strMsg = strprintf(_("Loading UTXO snapshot (%d%%)..."),
i * 100 / numUtxos);
uiInterface.InitMessage(strMsg);
printf("UtxoSnapshot: loaded %d / %d UTXOs\n", i, numUtxos);
}
}
}
if (success && !flushBatch())
success = false;
}
// Verify content hash
if (success) {
uint256 actualHash;
SHA256_Final((unsigned char*)&actualHash, &sha256);
if (actualHash != expectedContentHash) {
success = false;
strError = "Content hash mismatch - snapshot may be corrupted";
}
}
// Write metadata
if (success) {
leveldb::WriteBatch metaBatch;
// hashBestChain
CDataStream ssKey1(SER_DISK, CLIENT_VERSION);
ssKey1 << std::string("hashBestChain");
CDataStream ssVal1(SER_DISK, CLIENT_VERSION);
ssVal1 << blockHash;
metaBatch.Put(ssKey1.str(), ssVal1.str());
// dbformat = 3
CDataStream ssKey2(SER_DISK, CLIENT_VERSION);
ssKey2 << std::string("dbformat");
CDataStream ssVal2(SER_DISK, CLIENT_VERSION);
ssVal2 << (int)3;
metaBatch.Put(ssKey2.str(), ssVal2.str());
// version
CDataStream ssKey3(SER_DISK, CLIENT_VERSION);
ssKey3 << std::string("version");
CDataStream ssVal3(SER_DISK, CLIENT_VERSION);
ssVal3 << DATABASE_VERSION;
metaBatch.Put(ssKey3.str(), ssVal3.str());
leveldb::Status s = pdb->Write(leveldb::WriteOptions(), &metaBatch);
if (!s.ok()) {
success = false;
strError = "Failed to write metadata: " + s.ToString();
}
}
// Clean up LevelDB
delete pdb;
delete options.filter_policy;
delete options.block_cache;
fclose(file);
if (!success) {
// Remove corrupted/incomplete database
printf("UtxoSnapshot: load failed: %s\n", strError.c_str());
if (fs::exists(txleveldbPath))
fs::remove_all(txleveldbPath);
return false;
}
printf("UtxoSnapshot: successfully loaded %d headers + %d UTXOs at height %d\n",
numHeaders, numUtxos, height);
return true;
}
} // namespace UtxoSnapshot
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) 2024-2025 Triangles developers
// Distributed under the MIT/X11 software license
#ifndef TRIANGLES_UTXOSNAPSHOT_H
#define TRIANGLES_UTXOSNAPSHOT_H
#include <string>
#include <boost/filesystem.hpp>
// UTXO snapshot file magic bytes
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
// UTXO snapshot format version
static const unsigned int UTXO_SNAPSHOT_VERSION = 1;
// Number of block index entries to include in snapshot (covers difficulty,
// median time, stake modifier, and reorg depth requirements)
static const unsigned int UTXO_SNAPSHOT_DEFAULT_HEADERS = 2000;
namespace UtxoSnapshot {
// Create a UTXO snapshot from the current chain state.
// Writes last nHeaders block index entries + all UTXOs to destPath.
// Returns true on success, sets strError on failure.
bool DumpSnapshot(const boost::filesystem::path& destPath,
unsigned int nHeaders,
std::string& strError);
// Load a UTXO snapshot from a file into a fresh LevelDB.
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
// The LevelDB must NOT be open yet (call before LoadBlockIndex).
// Returns true on success, sets strError on failure.
bool LoadSnapshot(const boost::filesystem::path& snapshotPath,
const boost::filesystem::path& dataDir,
std::string& strError);
} // namespace UtxoSnapshot
#endif // TRIANGLES_UTXOSNAPSHOT_H