Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df054bbcb | |||
| fbd931a392 | |||
| a792f90489 | |||
| 64939a9793 | |||
| b506a48192 | |||
| dee0d9ef62 |
+22
-1
@@ -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 "")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user