Compare commits

...

6 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
20 changed files with 1721 additions and 128 deletions
+22 -1
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif()
project(Triangles
VERSION 5.8.6
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")
+35
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>
@@ -766,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
+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 6
#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()))
+503 -76
View File
@@ -77,6 +77,8 @@ int64_t nTimeBestReceived = 0;
CMedianFilter<int> cPeerBlockCounts(5, 0); // Amount of blocks that other nodes claim to have
CScriptVerifyCache scriptVerifyCache;
map<uint256, CBlock*> mapOrphanBlocks;
multimap<uint256, CBlock*> mapOrphanBlocksByPrev;
set<pair<COutPoint, unsigned int> > setStakeSeenOrphan;
@@ -85,6 +87,19 @@ set<pair<COutPoint, unsigned int> > setStakeSeenOrphan;
map<uint256, CTransaction> mapOrphanTransactions;
map<uint256, set<uint256> > mapOrphanTransactionsByPrev;
// Compact block relay: partial blocks awaiting missing transactions
struct CPartialBlock
{
CCompactBlock cmpctblock;
std::vector<CTransaction> vTxFilled; // filled transactions (indexed by position)
std::set<uint16_t> setMissing; // indices still needed
int64_t nReceiveTime;
CNode* pfrom;
};
static std::map<uint256, CPartialBlock> mapPartialBlocks;
static const unsigned int MAX_PARTIAL_BLOCKS = 5;
static const int64_t PARTIAL_BLOCK_TTL = 30; // seconds
// Constant stuff for coinbase transactions we create:
CScript COINBASE_FLAGS;
@@ -110,6 +125,8 @@ struct CHeaderSyncNode
uint256 nChainTrust;
bool fRequested;
int64_t nLastRequestTime;
int64_t nFirstRequestTime; // when this block was first requested (for latency tracking)
int64_t nInsertTime;
};
static std::map<uint256, CHeaderSyncNode> mapHeaderSync;
@@ -117,11 +134,12 @@ static uint256 hashBestHeaderSync = 0;
static CCriticalSection cs_PostIbdWork;
static bool fPostIbdWorkStarted = false;
static const unsigned int MAX_HEADER_SYNC_CACHE = 50000;
static const unsigned int MAX_HEADER_SYNC_CACHE = 15000;
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 const unsigned int HEADER_DOWNLOAD_PER_PEER = 32; // Reduced from 64 for Tor circuit stability
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 60 * 1000000; // 60s for Tor latency (was 30s)
static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 5 * 1000000; // 5s redundant request (reduced for Tor)
static const int64_t HEADER_SYNC_TTL_MICROS = 15 * 60 * 1000000; // 15-minute TTL for cache entries (extended for Tor latency)
static void ThreadPostIbdWork(void* parg)
{
@@ -232,12 +250,47 @@ static void RecomputeBestHeaderSync()
static void PruneHeaderSync()
{
if (mapHeaderSync.size() <= MAX_HEADER_SYNC_CACHE)
return;
const int64_t nNow = GetTime() * 1000000;
printf("IBD-DIAG: header sync cache exceeded %u entries, clearing planner state\n", MAX_HEADER_SYNC_CACHE);
mapHeaderSync.clear();
hashBestHeaderSync = 0;
// TTL eviction: remove entries older than 5 minutes
if (mapHeaderSync.size() > MAX_HEADER_SYNC_CACHE / 2)
{
unsigned int nEvicted = 0;
for (auto it = mapHeaderSync.begin(); it != mapHeaderSync.end(); )
{
if (nNow - it->second.nInsertTime >= HEADER_SYNC_TTL_MICROS)
{
it = mapHeaderSync.erase(it);
++nEvicted;
}
else
++it;
}
if (nEvicted > 0)
{
printf("IBD-DIAG: TTL-evicted %u stale header sync entries, %u remain\n",
nEvicted, (unsigned int)mapHeaderSync.size());
RecomputeBestHeaderSync();
}
}
// Hard limit: if still over max, evict oldest entries
if (mapHeaderSync.size() > MAX_HEADER_SYNC_CACHE)
{
printf("IBD-DIAG: header sync cache exceeded %u entries, evicting oldest\n", MAX_HEADER_SYNC_CACHE);
while (mapHeaderSync.size() > MAX_HEADER_SYNC_CACHE * 3 / 4)
{
// Find oldest entry by insert time
auto oldest = mapHeaderSync.begin();
for (auto it = mapHeaderSync.begin(); it != mapHeaderSync.end(); ++it)
{
if (it->second.nInsertTime < oldest->second.nInsertTime)
oldest = it;
}
mapHeaderSync.erase(oldest);
}
RecomputeBestHeaderSync();
}
}
static bool AddHeaderSyncNode(const CBlock& header, const uint256& hashHeader)
@@ -283,6 +336,8 @@ static bool AddHeaderSyncNode(const CBlock& header, const uint256& hashHeader)
node.nChainTrust = nPrevChainTrust + GetHeaderSyncTrust(header.nBits);
node.fRequested = false;
node.nLastRequestTime = 0;
node.nFirstRequestTime = 0;
node.nInsertTime = GetTime() * 1000000;
mapHeaderSync.insert(std::make_pair(hashHeader, node));
@@ -386,6 +441,15 @@ static unsigned int QueueHeaderSyncBlocks(CNode* pfrom, unsigned int nWindow)
return nQueued;
}
// Returns the first request time (microseconds) for a block in the header sync cache, or 0
static int64_t GetHeaderSyncRequestTime(const uint256& hashBlock)
{
std::map<uint256, CHeaderSyncNode>::const_iterator mi = mapHeaderSync.find(hashBlock);
if (mi == mapHeaderSync.end())
return 0;
return mi->second.nFirstRequestTime;
}
static void MarkHeaderSyncBlockAccepted(const uint256& hashBlock)
{
std::map<uint256, CHeaderSyncNode>::iterator mi = mapHeaderSync.find(hashBlock);
@@ -455,6 +519,26 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
vWeightedPeers.push_back(vEligiblePeers[i]);
}
// Adaptive timeout: use average peer latency to set timeouts.
// If peers average 2s, timeout at 10s. If peers average 15s, timeout at 45s.
// Clamp between 10s and 60s. Default to 60s when no latency data.
int64_t nAdaptiveTimeout = HEADER_REQUEST_TIMEOUT_MICROS;
{
int64_t nTotalLatency = 0;
int nPeersWithLatency = 0;
for (const CNode* pnode : vEligiblePeers) {
if (pnode->nAvgBlockLatencyUs > 0) {
nTotalLatency += pnode->nAvgBlockLatencyUs;
++nPeersWithLatency;
}
}
if (nPeersWithLatency > 0) {
int64_t nAvgLatency = nTotalLatency / nPeersWithLatency;
nAdaptiveTimeout = std::max((int64_t)(10 * 1000000),
std::min((int64_t)(60 * 1000000), nAvgLatency * 5));
}
}
// Distribute blocks across peers using speed-weighted assignment
for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it)
{
@@ -465,16 +549,16 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
if (mi == mapHeaderSync.end())
continue;
// Check if already requested recently
// Check if already requested recently (using adaptive timeout)
bool fNeedsRequest = false;
if (!mi->second.fRequested)
{
// Never requested - request now
fNeedsRequest = true;
}
else if (nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
else if (nNow - mi->second.nLastRequestTime >= nAdaptiveTimeout)
{
// Timeout expired - retry
// Adaptive timeout expired - retry
fNeedsRequest = true;
}
else if (nNow - mi->second.nLastRequestTime >= HEADER_REDUNDANT_REQUEST_MICROS)
@@ -491,9 +575,22 @@ static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
pnode->AskFor(CInv(MSG_BLOCK, *it));
// During IBD with 2+ peers: also request from a second peer immediately.
// Doubles bandwidth but halves worst-case latency when one peer is slow.
// The AlreadyHave() check in getdata construction automatically skips
// the duplicate once the first response arrives.
if (IsInitialBlockDownload() && vWeightedPeers.size() >= 2 && !mi->second.fRequested)
{
CNode* pnode2 = vWeightedPeers[(nPeerIndex + 1) % vWeightedPeers.size()];
if (pnode2 != pnode)
pnode2->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)
{
if (!mi->second.fRequested)
mi->second.nFirstRequestTime = nNow;
mi->second.fRequested = true;
mi->second.nLastRequestTime = nNow;
}
@@ -1900,6 +1997,7 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
// The first loop above does all the inexpensive checks.
// Only if ALL inputs pass do we perform expensive ECDSA signature checks.
// Helps prevent CPU exhaustion attacks.
const uint256 hashTx = GetHash();
for (unsigned int i = 0; i < vin.size(); i++)
{
COutPoint prevout = vin[i].prevout;
@@ -1910,6 +2008,11 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
// still computed and checked, and any change will be caught at the next checkpoint.
if (!(fBlock && (nBestHeight < Checkpoints::GetTotalBlocksEstimate())))
{
// Check signature cache: skip re-verification for scripts already
// validated during mempool acceptance or prior block connections.
if (scriptVerifyCache.Get(hashTx, i))
continue;
if (pvChecks)
{
pvChecks->push_back(CScriptCheck(entry.scriptPubKey, vin[i].scriptSig, *this, i, 0));
@@ -1919,6 +2022,7 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
// Verify signature using scriptPubKey from UTXO entry
if (!VerifyScript(vin[i].scriptSig, entry.scriptPubKey, *this, i, 0))
return DoS(100, error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,10).c_str()));
scriptVerifyCache.Set(hashTx, i);
}
}
}
@@ -2286,7 +2390,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
if (!tx.ConnectInputs(txdb, mapInputs, pindex, true, false,
pScriptCheckQueue ? &vChecks : NULL))
return false;
if (pScriptCheckQueue && vChecks.size() >= 128)
if (pScriptCheckQueue && vChecks.size() >= 32)
{
scriptcheckcontrol.Add(vChecks);
vChecks.clear();
@@ -2526,6 +2630,27 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
nDisconnectDepth, MAX_REORG_DEPTH, pfork->nHeight);
return error("Reorganize() : reorg depth %u exceeds maximum %u", nDisconnectDepth, MAX_REORG_DEPTH);
}
// Deep reorgs (>6 blocks): require 10% more cumulative trust.
// Shallow reorgs (1-6 blocks) converge freely so nodes don't
// get stuck on their own fork. Deep reorgs need a substantial
// trust advantage to prevent long-range attacks.
if (nDisconnectDepth > 6)
{
CBigNum bnNewTrust(pindexNew->nChainTrust);
CBigNum bnBestTrust(pindexBest->nChainTrust);
if (bnNewTrust * 10 <= bnBestTrust * 11)
{
printf("REORGANIZE: REJECTED — deep reorg (%u blocks) has insufficient trust delta "
"(need >10%%, have %s vs %s)\n",
nDisconnectDepth,
bnNewTrust.ToString().c_str(),
bnBestTrust.ToString().c_str());
return error("Reorganize() : deep reorg %u blocks with insufficient trust delta",
nDisconnectDepth);
}
printf("REORGANIZE: Deep reorg (%u blocks) accepted — trust delta sufficient\n",
nDisconnectDepth);
}
}
// List of what to disconnect
@@ -3032,12 +3157,11 @@ 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. 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.
// 1. Strictly greater trust always wins (normal case).
// Deep reorgs are further gated by a 10% trust-delta check
// inside Reorganize() to prevent long-range attacks while
// still allowing natural short-fork convergence.
// 2. Equal trust: deterministic tiebreaker with timestamp preference.
// First prefer the block with the earlier timestamp (lower nTime),
// then break remaining ties by lower hash. This converges faster
// because the earlier block is more likely to have propagated first.
@@ -3045,33 +3169,7 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
bool fNewBest = false;
static int64_t nLastEqualTrustReorg = 0;
if (pindexNew->nChainTrust > nBestChainTrust)
{
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());
}
}
}
fNewBest = true;
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
GetTime() - nLastEqualTrustReorg > 2 * 60)
{
@@ -3298,17 +3396,52 @@ bool CBlock::AcceptBlock()
// 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.
// to a single hop. Uses nBestKnownHeight (updated from inv/block msgs)
// rather than nStartingHeight (static, set at connect time only).
//
// For peers with fPreferHeaders (sendheaders negotiated), send a header
// announcement — saves one round-trip vs inv->getdata->block.
if (hashBestChain == hash)
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
if (pnode->nStartingHeight >= nBestHeight - 10)
{
if (!pnode->fSuccessfullyConnected)
continue;
bool fNearTip = (pnode->nBestKnownHeight >= nBestHeight - 10) ||
(pnode->nBlocksDelivered > 0);
if (fNearTip && pnode->fSendCmpct)
{
// Compact block push: header + prefilled coinbase/coinstake +
// short IDs for remaining txs. For typical PoS blocks (0-2 txs)
// this is the complete block — no follow-up needed.
CCompactBlock cmpctblk(*this);
pnode->PushMessage("cmpctblock", cmpctblk);
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
}
else if (fNearTip)
{
// Direct full block push to near-tip peers
pnode->PushMessage("block", *this);
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
}
else if (pnode->fPreferHeaders)
{
// Header announcement for peers that requested sendheaders.
// Construct a header-only CBlock (no transactions/signature).
CBlock hdr;
hdr.nVersion = nVersion;
hdr.hashPrevBlock = hashPrevBlock;
hdr.hashMerkleRoot = hashMerkleRoot;
hdr.nTime = nTime;
hdr.nBits = nBits;
hdr.nNonce = nNonce;
std::vector<CBlock> vHeaders(1, hdr);
pnode->PushMessage("headers", vHeaders);
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
}
}
}
return true;
@@ -4328,8 +4461,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
pfrom->PushAddress(addr);
}
// Get recent addresses
if (pfrom->fOneShot || pfrom->nVersion >= CADDR_TIME_VERSION || addrman.size() < 1000)
// Always request addresses — critical for Tor-only small networks
{
pfrom->PushMessage("getaddr");
pfrom->fGetAddr = true;
@@ -4341,6 +4473,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
addrman.Add(addrFrom, addrFrom);
addrman.Good(addrFrom);
}
// Also request addresses from inbound peers (small network optimization)
if (!pfrom->fGetAddr) {
pfrom->PushMessage("getaddr");
pfrom->fGetAddr = true;
}
}
// Ask connected nodes for block updates.
@@ -4385,6 +4522,10 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
pfrom->fSuccessfullyConnected = true;
// Request header-based block announcements and compact block relay
pfrom->PushMessage("sendheaders");
pfrom->PushMessage("sendcmpct");
// If this is an onion peer and we have a pending resolve, request their wallet address
{
std::string peerAddr = pfrom->addr.ToStringIP();
@@ -4417,6 +4558,22 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
}
else if (strCommand == "sendheaders")
{
// Peer prefers block announcements via headers instead of inv.
// When we have a new block, we'll send a "headers" message rather
// than waiting for the inv->getdata round-trip, saving ~2-4s on Tor.
pfrom->fPreferHeaders = true;
}
else if (strCommand == "sendcmpct")
{
// Peer supports compact block relay
pfrom->fSendCmpct = true;
}
else if (strCommand == "addr")
{
vector<CAddress> vAddr;
@@ -4527,6 +4684,8 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
int h = mi->second->nHeight;
if (nFirstInvHeight == -1) nFirstInvHeight = h;
nLastInvHeight = h;
if (h > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = h;
if (h > nBestHeight) nAboveBest++;
}
} else {
@@ -4910,8 +5069,24 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
CInv inv(MSG_BLOCK, hashBlock);
pfrom->AddInventoryKnown(inv);
// Track block delivery for peer latency scoring
// Track block delivery and measure latency for adaptive timeouts
pfrom->nBlocksDelivered++;
if (nBestHeight > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = nBestHeight;
// Update rolling average latency (exponential moving average, 7/8 old + 1/8 new)
{
int64_t nRequestTime = GetHeaderSyncRequestTime(hashBlock);
if (nRequestTime > 0) {
int64_t nLatency = GetTime() * 1000000 - nRequestTime;
if (nLatency > 0) {
if (pfrom->nAvgBlockLatencyUs == 0)
pfrom->nAvgBlockLatencyUs = nLatency;
else
pfrom->nAvgBlockLatencyUs = (pfrom->nAvgBlockLatencyUs * 7 + nLatency) / 8;
}
}
}
if (ProcessBlock(pfrom, &block))
{
@@ -4919,8 +5094,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
if (IsInitialBlockDownload())
{
// Keep download window full after every accepted block
QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW);
static int nBlocksSinceRequest = 0;
if (++nBlocksSinceRequest >= 5000)
if (++nBlocksSinceRequest >= 500)
{
nBlocksSinceRequest = 0;
// Pipeline refill: request from ALL connected full-node peers,
@@ -4961,6 +5139,198 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
}
else if (strCommand == "cmpctblock")
{
CCompactBlock cmpctblock;
vRecv >> cmpctblock;
uint256 hashBlock = cmpctblock.GetBlockHash();
CInv inv(MSG_BLOCK, hashBlock);
pfrom->AddInventoryKnown(inv);
// Skip if we already have this block
if (mapBlockIndex.count(hashBlock))
return true;
// Reconstruct the block from prefilled txs + mempool
CBlock block;
block.nVersion = cmpctblock.nVersion;
block.hashPrevBlock = cmpctblock.hashPrevBlock;
block.hashMerkleRoot = cmpctblock.hashMerkleRoot;
block.nTime = cmpctblock.nTime;
block.nBits = cmpctblock.nBits;
block.nNonce = cmpctblock.nNonce;
block.vchBlockSig = cmpctblock.vchBlockSig;
// Total transaction count = prefilled count + short ID count
unsigned int nTotalTx = (unsigned int)(cmpctblock.vPrefilledTxn.size() + cmpctblock.vShortTxIds.size());
block.vtx.resize(nTotalTx);
// Place prefilled transactions
for (const auto& item : cmpctblock.vPrefilledTxn)
{
if (item.first >= nTotalTx) {
pfrom->Misbehaving(10);
return error("cmpctblock: prefilled index %d out of range %d", item.first, nTotalTx);
}
block.vtx[item.first] = item.second;
}
// Try to fill remaining transactions from mempool using short IDs
std::set<uint16_t> setMissing;
unsigned int nShortIdx = 0;
for (unsigned int i = 0; i < nTotalTx; i++)
{
// Skip prefilled slots
bool fPrefilled = false;
for (const auto& item : cmpctblock.vPrefilledTxn) {
if (item.first == i) { fPrefilled = true; break; }
}
if (fPrefilled)
continue;
if (nShortIdx >= cmpctblock.vShortTxIds.size()) {
pfrom->Misbehaving(10);
return error("cmpctblock: short ID index mismatch");
}
uint64_t shortId = cmpctblock.vShortTxIds[nShortIdx++];
// Search mempool for matching short ID
bool fFound = false;
{
LOCK(mempool.cs);
for (const auto& entry : mempool.mapTx)
{
if (GetShortTxId(entry.first, cmpctblock.nShortIdNonce) == shortId)
{
block.vtx[i] = entry.second;
fFound = true;
break;
}
}
}
if (!fFound)
setMissing.insert(i);
}
if (setMissing.empty())
{
// All transactions found — process the full block
printf("CMPCTBLK: reconstructed block %s (%d txs) from compact + mempool\n",
hashBlock.ToString().substr(0,20).c_str(), nTotalTx);
pfrom->nBlocksDelivered++;
if (nBestHeight > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = nBestHeight;
ProcessBlock(pfrom, &block);
mapAlreadyAskedFor.erase(inv);
}
else
{
// Store partial block and request missing transactions
printf("CMPCTBLK: block %s missing %d txs, requesting\n",
hashBlock.ToString().substr(0,20).c_str(), (int)setMissing.size());
// Evict oldest partial blocks if at limit
while (mapPartialBlocks.size() >= MAX_PARTIAL_BLOCKS)
{
auto oldest = mapPartialBlocks.begin();
for (auto it = mapPartialBlocks.begin(); it != mapPartialBlocks.end(); ++it)
if (it->second.nReceiveTime < oldest->second.nReceiveTime)
oldest = it;
mapPartialBlocks.erase(oldest);
}
CPartialBlock partial;
partial.cmpctblock = cmpctblock;
partial.vTxFilled = block.vtx;
partial.setMissing = setMissing;
partial.nReceiveTime = GetTime();
partial.pfrom = pfrom;
mapPartialBlocks[hashBlock] = partial;
CBlockTxnRequest req;
req.blockhash = hashBlock;
req.vIndex.assign(setMissing.begin(), setMissing.end());
pfrom->PushMessage("getblocktxn", req);
}
}
else if (strCommand == "getblocktxn")
{
CBlockTxnRequest req;
vRecv >> req;
// Look up the block and send requested transactions
map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(req.blockhash);
if (mi != mapBlockIndex.end())
{
CBlock block;
if (block.ReadFromDisk(mi->second))
{
CBlockTxnResponse resp;
resp.blockhash = req.blockhash;
for (uint16_t idx : req.vIndex)
{
if (idx < block.vtx.size())
resp.vTxn.push_back(block.vtx[idx]);
}
pfrom->PushMessage("blocktxn", resp);
}
}
}
else if (strCommand == "blocktxn")
{
CBlockTxnResponse resp;
vRecv >> resp;
// Find the partial block awaiting these transactions
auto mi = mapPartialBlocks.find(resp.blockhash);
if (mi == mapPartialBlocks.end())
return true; // no longer need it
CPartialBlock& partial = mi->second;
unsigned int nFilled = 0;
auto itTxn = resp.vTxn.begin();
for (uint16_t idx : partial.setMissing)
{
if (itTxn == resp.vTxn.end())
break;
if (idx < partial.vTxFilled.size())
{
partial.vTxFilled[idx] = *itTxn;
nFilled++;
}
++itTxn;
}
partial.setMissing.clear(); // all filled now
// Reconstruct and process the complete block
CBlock block;
block.nVersion = partial.cmpctblock.nVersion;
block.hashPrevBlock = partial.cmpctblock.hashPrevBlock;
block.hashMerkleRoot = partial.cmpctblock.hashMerkleRoot;
block.nTime = partial.cmpctblock.nTime;
block.nBits = partial.cmpctblock.nBits;
block.nNonce = partial.cmpctblock.nNonce;
block.vchBlockSig = partial.cmpctblock.vchBlockSig;
block.vtx = partial.vTxFilled;
printf("CMPCTBLK: completed block %s with %d missing txs from blocktxn\n",
resp.blockhash.ToString().substr(0,20).c_str(), nFilled);
pfrom->nBlocksDelivered++;
if (nBestHeight > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = nBestHeight;
ProcessBlock(pfrom, &block);
mapAlreadyAskedFor.erase(CInv(MSG_BLOCK, resp.blockhash));
mapPartialBlocks.erase(mi);
}
else if (strCommand == "getaddr")
{
// Don't return addresses older than nCutOff timestamp
@@ -5042,22 +5412,38 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
{
uint64_t nonce = 0;
vRecv >> nonce;
// Echo the message back with the nonce. This allows for two useful features:
//
// 1) A remote node can quickly check if the connection is operational
// 2) Remote nodes can measure the latency of the network thread. If this node
// is overloaded it won't respond to pings quickly and the remote node can
// avoid sending us more work, like chain download requests.
//
// The nonce stops the remote getting confused between different pings: without
// it, if the remote node sends a ping once per second and this node takes 5
// seconds to respond to each, the 5th ping the remote sends would appear to
// return very quickly.
pfrom->PushMessage("pong", nonce);
}
}
else if (strCommand == "pong")
{
if (pfrom->nVersion > BIP0031_VERSION)
{
uint64_t nonce = 0;
vRecv >> nonce;
// Only accept pong if it matches our outstanding ping nonce
if (nonce != 0 && nonce == pfrom->nPingNonceSent) {
int64_t nRtt = GetTimeMicros() - pfrom->nPingUsecStart;
if (nRtt > 0) {
pfrom->nPingUsecTime = nRtt;
// Update rolling average block latency if not set
if (pfrom->nAvgBlockLatencyUs == 0)
pfrom->nAvgBlockLatencyUs = nRtt;
else
pfrom->nAvgBlockLatencyUs = (pfrom->nAvgBlockLatencyUs * 3 + nRtt) / 4;
}
pfrom->nPingNonceSent = 0;
pfrom->nPingUsecStart = 0;
pfrom->nPingRetryCount = 0;
if (fDebug)
printf("pong from %s: %.1fms\n", pfrom->addr.ToString().c_str(), (double)nRtt / 1000.0);
}
}
}
else if (strCommand == "alert")
{
CAlert alert;
@@ -5197,7 +5583,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
// Update the last seen time for this node's address
if (pfrom->fNetworkNode)
if (strCommand == "version" || strCommand == "addr" || strCommand == "inv" || strCommand == "getdata" || strCommand == "ping")
if (strCommand == "version" || strCommand == "addr" || strCommand == "inv" || strCommand == "getdata" || strCommand == "ping" || strCommand == "pong")
AddressCurrentlyConnected(pfrom->addr);
@@ -5325,22 +5711,48 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
if (pto->nVersion == 0)
return true;
// Keep-alive ping. We send a nonce of zero because we don't use it anywhere
// right now.
if (pto->nLastSend && GetTime() - pto->nLastSend > 30 * 60 && pto->ssSend.empty()) {
uint64_t nonce = 0;
if (pto->nVersion > BIP0031_VERSION)
pto->PushMessage("ping", nonce);
else
pto->PushMessage("ping");
// Keep-alive ping every 2 minutes (critical for Tor connections that
// can be silently dropped). Also measures round-trip latency.
{
bool fPingNeeded = false;
// Send ping every 2 minutes if no recent send activity
if (pto->nLastSend && GetTime() - pto->nLastSend > 120 && pto->ssSend.empty())
fPingNeeded = true;
// Also ping if we haven't sent one in 2 minutes regardless
if (pto->nPingUsecStart == 0 && GetTime() - pto->nTimeConnected > 120)
fPingNeeded = true;
if (pto->nPingUsecStart > 0 && GetTimeMicros() - pto->nPingUsecStart > 120 * 1000000)
fPingNeeded = true; // outstanding ping timed out, retry
if (fPingNeeded) {
// Check for dead peer: 3 consecutive unanswered pings = disconnect
if (pto->nPingNonceSent != 0 && pto->nPingUsecStart > 0) {
pto->nPingRetryCount++;
if (pto->nPingRetryCount >= 3) {
printf("ping timeout: %s (no pong for %d pings, %.1fs)\n",
pto->addr.ToString().c_str(), pto->nPingRetryCount,
(double)(GetTimeMicros() - pto->nPingUsecStart) / 1000000.0);
pto->fDisconnect = true;
}
}
if (!pto->fDisconnect) {
uint64_t nonce = 0;
while (nonce == 0)
RAND_bytes((unsigned char*)&nonce, sizeof(nonce));
pto->nPingNonceSent = nonce;
pto->nPingUsecStart = GetTimeMicros();
pto->PushMessage("ping", nonce);
}
}
}
// Resend wallet transactions that haven't gotten in a block yet
ResendWalletTransactions();
// Address refresh broadcast
// Address refresh broadcast — every hour for small Tor-only networks
// (was 24 hours, but small networks need faster address propagation)
static int64_t nLastRebroadcast;
if (!IsInitialBlockDownload() && (GetTime() - nLastRebroadcast > 24 * 60 * 60))
if (!IsInitialBlockDownload() && (GetTime() - nLastRebroadcast > 60 * 60))
{
{
LOCK(cs_vNodes);
@@ -5357,6 +5769,14 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
if (addr.IsRoutable())
pnode->PushAddress(addr);
}
// Periodically re-request addresses (every hour)
// Helps small networks discover all peers faster
if (!pnode->fGetAddr && pnode->fSuccessfullyConnected)
{
pnode->PushMessage("getaddr");
pnode->fGetAddr = true;
}
}
}
nLastRebroadcast = GetTime();
@@ -5516,7 +5936,14 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
static int64_t nLastBlockReceived = 0;
static int nLastHeight = 0;
static int64_t nLastStallLog = 0;
int nStallTimeout = IsInitialBlockDownload() ? 5 : 15;
// Adaptive stall timeout: use peer's latency if known
int nStallTimeout;
if (pto->nAvgBlockLatencyUs > 0) {
// 5x average latency, clamped to 5-60 seconds
nStallTimeout = std::max(5, std::min(60, (int)(pto->nAvgBlockLatencyUs * 5 / 1000000)));
} else {
nStallTimeout = IsInitialBlockDownload() ? 10 : 30;
}
if (nBestHeight > nLastHeight) {
nLastHeight = nBestHeight;
nLastBlockReceived = GetTime();
+124 -3
View File
@@ -12,6 +12,7 @@
#include "scrypt.h"
#include "hashblock.h"
#include "checkqueue.h"
#include "sigcache.h"
#include <list>
@@ -37,8 +38,8 @@ 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;
@@ -1687,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.
@@ -1711,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)
+87 -29
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);
@@ -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;
}
}
@@ -1432,16 +1455,12 @@ void ThreadOnionSeed(void* parg)
printf("ThreadOnionSeed: initial seeding complete\n");
// 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.
// 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 < 120 && !fShutdown; i++) // sleep 2 minutes
MilliSleep(1000);
if (fShutdown) break;
// Count outbound peers to determine check interval
int nOutbound = 0;
{
LOCK(cs_vNodes);
@@ -1450,12 +1469,44 @@ void ThreadOnionSeed(void* parg)
nOutbound++;
}
int64_t nCooldown = bFirstReseed ? 5 * 60 : 15 * 60;
// 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) {
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
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);
// Also re-queue hardcoded seeds for direct connection
// 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());
@@ -2341,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);
}
@@ -2412,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;
+18 -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:
@@ -275,8 +279,15 @@ public:
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;
mruset<CAddress> setAddrKnown;
@@ -314,6 +325,7 @@ public:
fSuccessfullyConnected = false;
fDisconnect = false;
fPreferHeaders = false;
fSendCmpct = false;
nRefCount = 0;
nSendSize = 0;
nSendOffset = 0;
@@ -326,7 +338,12 @@ public:
nLastTipCheck = 0;
nAvgBlockLatencyUs = 0;
nBlocksDelivered = 0;
nBestKnownHeight = -1;
nIncompatibleGetblocks = 0;
nPingNonceSent = 0;
nPingUsecStart = 0;
nPingUsecTime = 0;
nPingRetryCount = 0;
fGetAddr = false;
nMisbehavior = 0;
hashCheckpointKnown = 0;
+47
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;
@@ -849,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;
}
+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
+2
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 },
@@ -319,6 +320,7 @@ static const CRPCCommand vRPCCommands[] =
{ "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},
+2
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);
@@ -227,6 +228,7 @@ extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fH
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);
+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