Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 019f5f33be | |||
| 66ac7e8537 | |||
| 93f879583f | |||
| b13139ad19 | |||
| d35aec1828 | |||
| 53a2f0b5d2 | |||
| 9cb44a2988 | |||
| c37102eff4 |
@@ -670,7 +670,16 @@ if(BUILD_TESTS)
|
||||
Boost::unit_test_framework
|
||||
)
|
||||
|
||||
# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}: the consensus_safety_tests
|
||||
# `reindex_reconstruction_is_explicit_and_fail_closed` test reads
|
||||
# src/init.cpp + src/main.cpp via __FILE__-relative path traversal
|
||||
# (3x parent_path() calls). When ctest runs from build/src/ (the
|
||||
# default CMAKE_CURRENT_BINARY_DIR for src/CMakeLists.txt), the
|
||||
# resolved path is build/src/src/init.cpp which doesn't exist.
|
||||
# Pinning WORKING_DIRECTORY to "${CMAKE_SOURCE_DIR}" makes the test
|
||||
# source paths resolve correctly from any environment.
|
||||
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
|
||||
set_tests_properties(triangles_unit_tests PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# ── Standalone chaindb equivalence tests ─────────────────────────────────
|
||||
# Runs without the TestingSetup global fixture (which would otherwise
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
#define CLIENT_VERSION_MAJOR 6
|
||||
#define CLIENT_VERSION_MINOR 2
|
||||
#define CLIENT_VERSION_REVISION 6
|
||||
#define CLIENT_VERSION_BUILD 1
|
||||
#define CLIENT_VERSION_BUILD 2
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
// Don't merge these into one macro!
|
||||
|
||||
+282
-38
@@ -401,16 +401,42 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
{
|
||||
if (running.load()) return true;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 0: validate input BEFORE any state mutation.
|
||||
// If validation fails, we must leave the system in a clean state
|
||||
// (running=false, no i2p data dir side effects, no InitI2P call).
|
||||
// ----------------------------------------------------------------
|
||||
if (socks < 1 || socks > 65535) {
|
||||
lastError = strprintf("SOCKS proxy port %d out of range (1-65535)", socks);
|
||||
return false;
|
||||
}
|
||||
if (sam < 1 || sam > 65535) {
|
||||
lastError = strprintf("SAM bridge port %d out of range (1-65535)", sam);
|
||||
return false;
|
||||
}
|
||||
if (server < 0 || server > 65535) {
|
||||
lastError = strprintf("server tunnel port %d out of range (0-65535, 0=disable)", server);
|
||||
return false;
|
||||
}
|
||||
|
||||
lastError.clear();
|
||||
socksPort = socks;
|
||||
samPort = sam;
|
||||
serverPort = server;
|
||||
i2pHostname.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
i2pHostname.clear();
|
||||
}
|
||||
|
||||
// Prepare i2pd data directory under the wallet's data dir
|
||||
i2pDataDir = (::GetDataDir() / "i2p_data").string();
|
||||
fs::create_directories(i2pDataDir);
|
||||
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
try {
|
||||
fs::create_directories(i2pDataDir);
|
||||
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
lastError = strprintf("Cannot create i2p data dir %s: %s", i2pDataDir.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Embedded I2P: starting i2pd router...\n");
|
||||
|
||||
@@ -448,6 +474,13 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
conf << "address = 127.0.0.1\n";
|
||||
conf << "port = " << samPort << "\n";
|
||||
conf << "\n";
|
||||
// Disable HTTP proxy (port 4444). Cycle-13 fix: the HTTPProxy runs by
|
||||
// default in i2pd 2.60.0 and any HTTP request to its port causes a
|
||||
// nullptr deref in i2p::i18n::Locale::GetString. Conf is dead code in
|
||||
// the embedded library path; SetOption in InitI2P is the real override.
|
||||
conf << "[httpproxy]\n";
|
||||
conf << "enabled = false\n";
|
||||
conf << "\n";
|
||||
// Disable HTTP webconsole (not needed for embedded use)
|
||||
conf << "[http]\n";
|
||||
conf << "enabled = false\n";
|
||||
@@ -470,20 +503,29 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
if (serverPort > 0) {
|
||||
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
|
||||
std::ofstream tunnelConf(tunnelConfPath.string());
|
||||
if (tunnelConf.is_open()) {
|
||||
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
|
||||
tunnelConf << "[triangles-p2p]\n";
|
||||
tunnelConf << "type = server\n";
|
||||
tunnelConf << "host = 127.0.0.1\n";
|
||||
tunnelConf << "port = " << serverPort << "\n";
|
||||
tunnelConf << "keys = triangles-p2p-keys.dat\n";
|
||||
tunnelConf << "inbound.length = 3\n";
|
||||
tunnelConf << "outbound.length = 3\n";
|
||||
tunnelConf << "inbound.quantity = 5\n";
|
||||
tunnelConf << "outbound.quantity = 5\n";
|
||||
tunnelConf.close();
|
||||
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
|
||||
if (!tunnelConf.is_open()) {
|
||||
lastError = strprintf("Failed to write %s for server tunnel configuration",
|
||||
tunnelConfPath.string().c_str());
|
||||
printf("ERROR: %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
|
||||
tunnelConf << "[triangles-p2p]\n";
|
||||
tunnelConf << "type = server\n";
|
||||
tunnelConf << "host = 127.0.0.1\n";
|
||||
tunnelConf << "port = " << serverPort << "\n";
|
||||
tunnelConf << "keys = triangles-p2p-keys.dat\n";
|
||||
tunnelConf << "inbound.length = 3\n";
|
||||
tunnelConf << "outbound.length = 3\n";
|
||||
tunnelConf << "inbound.quantity = 5\n";
|
||||
tunnelConf << "outbound.quantity = 5\n";
|
||||
tunnelConf.close();
|
||||
if (tunnelConf.fail()) {
|
||||
lastError = strprintf("Write failed for %s", tunnelConfPath.string().c_str());
|
||||
printf("ERROR: %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
|
||||
}
|
||||
|
||||
// Build argv for i2pd initialization. Pass --datadir and --conf on the
|
||||
@@ -501,14 +543,36 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
argvPtrs.push_back(&s[0]);
|
||||
argvPtrs.push_back(nullptr);
|
||||
|
||||
// The whole post-InitI2P section is wrapped in try/catch so that ANY
|
||||
// failure after i2pd is initialized triggers TerminateI2P. Without this
|
||||
// an exception from SetOption or std::thread construction would leave
|
||||
// running=true but with no router thread to clean up — a leaked i2pd.
|
||||
try {
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 1 (synchronous, < 1s): config parse, crypto, router context
|
||||
// Phase 1 (synchronous, < 1s): config parse, crypto, router context.
|
||||
// InitI2P is wrapped in try/catch so a partial-init failure does
|
||||
// not leave i2pd in a half-initialized state with running=true.
|
||||
// ----------------------------------------------------------------
|
||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||
try {
|
||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||
} catch (const std::exception& e) {
|
||||
lastError = strprintf("InitI2P failed: %s", e.what());
|
||||
// Best-effort cleanup: i2pd's InitI2P may have partially
|
||||
// initialized global state. TerminateI2P is a no-op if no
|
||||
// init happened; it cleans up otherwise.
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
return false;
|
||||
} catch (...) {
|
||||
lastError = "InitI2P failed: unknown exception";
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
return false;
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
// Mark running immediately so Qt UI shows I2P as active.
|
||||
// From this point on, any exception thrown by the code below is
|
||||
// caught by the outer try/catch, which calls TerminateI2P to
|
||||
// release the partially-initialized i2pd state.
|
||||
running.store(true);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -548,17 +612,11 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
// but BEFORE the background thread calls i2p::client::context.Start
|
||||
// which calls ReadSocksProxy + ReadSAMBridge. SetOption calls
|
||||
// notify() internally, so the new values are visible to GetOption.
|
||||
//
|
||||
// NOTE: SOCKS/SAM port range validation happens in Start() Phase 0
|
||||
// before any state mutation, so by this point socksPort and samPort
|
||||
// are already known to be 1..65535. No re-validation needed here.
|
||||
// ----------------------------------------------------------------
|
||||
if (socksPort < 1 || socksPort > 65535) {
|
||||
lastError = strprintf("SOCKS proxy port %d out of range (1-65535)",
|
||||
socksPort);
|
||||
return false;
|
||||
}
|
||||
if (samPort < 1 || samPort > 65535) {
|
||||
lastError = strprintf("SAM bridge port %d out of range (1-65535)",
|
||||
samPort);
|
||||
return false;
|
||||
}
|
||||
printf("Embedded I2P: overriding socksproxy.port=%d sam.port=%d via SetOption\n",
|
||||
socksPort, samPort);
|
||||
fflush(stdout);
|
||||
@@ -570,7 +628,14 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
bool samEnabled = true;
|
||||
std::string samAddr = "127.0.0.1";
|
||||
uint16_t samPortVal = (uint16_t)samPort;
|
||||
bool httpEnabled = false;
|
||||
// Cycle-13 fix: HTTPProxy runs by default in i2pd 2.60.0 on
|
||||
// port 4444 and any inbound HTTP request crashes the daemon via
|
||||
// nullptr deref in i2p::i18n::Locale::GetString (m_Language is
|
||||
// never initialized). The previous SetOption("http.enabled",...)
|
||||
// targeted the i2pd WEBCONSOLE, not the HTTPProxy. Correct key
|
||||
// is "httpproxy.enabled".
|
||||
bool httpproxyEnabled = false;
|
||||
bool httpWebconsoleEnabled = false;
|
||||
bool i2pcontrolEnabled = false;
|
||||
bool bobEnabled = false;
|
||||
|
||||
@@ -581,7 +646,9 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
i2p::config::SetOption("sam.enabled", samEnabled);
|
||||
i2p::config::SetOption("sam.address", samAddr);
|
||||
i2p::config::SetOption("sam.port", samPortVal);
|
||||
i2p::config::SetOption("http.enabled", httpEnabled);
|
||||
// Cycle-13 fix: was "http.enabled" which targeted webconsole.
|
||||
i2p::config::SetOption("httpproxy.enabled", httpproxyEnabled);
|
||||
i2p::config::SetOption("http.enabled", httpWebconsoleEnabled);
|
||||
i2p::config::SetOption("i2pcontrol.enabled", i2pcontrolEnabled);
|
||||
i2p::config::SetOption("bob.enabled", bobEnabled);
|
||||
}
|
||||
@@ -665,19 +732,35 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
}
|
||||
}
|
||||
|
||||
// Populate .b32.i2p address
|
||||
try {
|
||||
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
|
||||
i2pHostname = identHash.ToBase32() + ".b32.i2p";
|
||||
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
|
||||
} catch (...) {
|
||||
printf("Embedded I2P: .b32.i2p address not yet available, Qt timer will retry\n");
|
||||
}
|
||||
// ----------------------------------------------------------------
|
||||
// Populate .b32.i2p address — use the SERVER TUNNEL destination,
|
||||
// NOT the embedded router identity. Delegates to
|
||||
// DiscoverServerTunnelDestination() which is also callable
|
||||
// from GetI2PAddress() so the Qt timerI2P retry path picks
|
||||
// up the result once the tunnel registers.
|
||||
// ----------------------------------------------------------------
|
||||
DiscoverServerTunnelDestination();
|
||||
fflush(stdout);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// Background init failure: i2pd router context may be partially
|
||||
// alive (transports listening, netDb half-built). Tear it down,
|
||||
// reset running, and surface the error in lastError so callers
|
||||
// can see the failure rather than seeing running=true forever.
|
||||
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
|
||||
fflush(stdout);
|
||||
lastError = std::string("i2pd background init failed: ") + e.what();
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
} catch (...) {
|
||||
// Catch-all: any non-std::exception (e.g. structured exception
|
||||
// on Windows) would otherwise invoke std::terminate, killing
|
||||
// the daemon with no useful diagnostic.
|
||||
printf("ERROR: Embedded I2P background init failed: unknown exception\n");
|
||||
fflush(stdout);
|
||||
lastError = "i2pd background init failed: unknown exception";
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -689,6 +772,16 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
} catch (const std::exception& e) {
|
||||
lastError = std::string("i2pd initialization failed: ") + e.what();
|
||||
printf("ERROR: Embedded I2P startup failed: %s\n", e.what());
|
||||
// i2pd may be partially or fully initialized by the time we got here.
|
||||
// TerminateI2P is a no-op if InitI2P never ran; otherwise it cleans
|
||||
// up router context, transports, and netDb.
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
return false;
|
||||
} catch (...) {
|
||||
lastError = "i2pd initialization failed: unknown exception";
|
||||
printf("ERROR: Embedded I2P startup failed: unknown exception\n");
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
return false;
|
||||
}
|
||||
@@ -736,6 +829,149 @@ void CI2PEmbedded::Stop()
|
||||
running.store(false);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CI2PEmbedded::DiscoverServerTunnelDestination
|
||||
//
|
||||
// Reads triangles-p2p-keys.dat (binary PrivateKeys blob) and looks for
|
||||
// a matching entry in i2p::client::context.GetServerTunnels(). On
|
||||
// match, sets i2pHostname to the corresponding ".b32.i2p" address.
|
||||
// On no match (or serverPort==0, or read failure), leaves i2pHostname
|
||||
// empty. Idempotent and safe to call repeatedly from the Qt timerI2P
|
||||
// path (qt/trianglesgui.cpp:384-387, default 5s interval).
|
||||
//
|
||||
// Why fail-closed: publishing the keys-file hash while the tunnel is
|
||||
// not yet registered would mean advertising a destination with no
|
||||
// published LeaseSet → peers hit SOCKS code 4 / "LeaseSet not found"
|
||||
// on the floodfill network. Empty hostname → no wrong-identity
|
||||
// connectivity. The Qt timer keeps retrying until the tunnel comes up.
|
||||
// ----------------------------------------------------------------
|
||||
void CI2PEmbedded::DiscoverServerTunnelDestination()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
|
||||
if (serverPort == 0) {
|
||||
// No P2P server tunnel configured. This is the -nolisten / no
|
||||
// -i2phsport case (pure outbound SOCKS I2P, no inbound service).
|
||||
if (!i2pHostname.empty()) {
|
||||
printf("Embedded I2P: serverPort=0, clearing previously "
|
||||
"discovered destination\n");
|
||||
i2pHostname.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool advertised = false;
|
||||
std::string serverKeysPath = (fs::path(i2pDataDir) / "triangles-p2p-keys.dat").string();
|
||||
|
||||
// Step 1: read expected ident hash from the keys file.
|
||||
std::string keysFileIdentB32;
|
||||
try {
|
||||
std::ifstream ks(serverKeysPath, std::ifstream::binary);
|
||||
if (ks.is_open()) {
|
||||
ks.seekg(0, std::ios::end);
|
||||
size_t len = ks.tellg();
|
||||
ks.seekg(0, std::ios::beg);
|
||||
if (len == 0 || len > 65536) {
|
||||
throw std::runtime_error("implausible keys file size: " +
|
||||
std::to_string(len));
|
||||
}
|
||||
std::vector<uint8_t> buf(len);
|
||||
ks.read(reinterpret_cast<char*>(buf.data()), len);
|
||||
if (!ks) {
|
||||
throw std::runtime_error("short read on keys file");
|
||||
}
|
||||
i2p::data::PrivateKeys pk;
|
||||
if (!pk.FromBuffer(buf.data(), len)) {
|
||||
throw std::runtime_error("PrivateKeys::FromBuffer failed");
|
||||
}
|
||||
auto pub = pk.GetPublic();
|
||||
if (!pub) {
|
||||
throw std::runtime_error("PrivateKeys::GetPublic returned null");
|
||||
}
|
||||
keysFileIdentB32 = pub->GetIdentHash().ToBase32();
|
||||
} else {
|
||||
// Quiet on retry — file-not-found is expected before the
|
||||
// bootstrap thread writes it for the first time.
|
||||
if (i2pHostname.empty()) {
|
||||
// First failure: log at info level so the operator can
|
||||
// see why the hostname is still empty.
|
||||
printf("Embedded I2P: cannot open %s for server tunnel keys "
|
||||
"(will retry on next timerI2P tick)\n",
|
||||
serverKeysPath.c_str());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
lastDiscoveryError = e.what();
|
||||
printf("Embedded I2P: keys-file ident hash load failed: %s\n", e.what());
|
||||
} catch (...) {
|
||||
lastDiscoveryError = "unknown exception";
|
||||
printf("Embedded I2P: keys-file ident hash load failed: unknown exception\n");
|
||||
}
|
||||
|
||||
// Step 2: only publish if a LIVE registered server tunnel matches
|
||||
// the keys-file hash. Registry membership confirms the tunnel is
|
||||
// active; LeaseSet publication is i2pd's responsibility after that.
|
||||
if (!keysFileIdentB32.empty()) {
|
||||
try {
|
||||
for (const auto& kv : i2p::client::context.GetServerTunnels()) {
|
||||
const i2p::data::IdentHash& dest = kv.first.first;
|
||||
if (dest.ToBase32() == keysFileIdentB32) {
|
||||
if (i2pHostname != keysFileIdentB32 + ".b32.i2p") {
|
||||
i2pHostname = keysFileIdentB32 + ".b32.i2p";
|
||||
lastDiscoveryError.clear(); // success — clear stale
|
||||
printf("Embedded I2P: server tunnel address "
|
||||
"(live registry) = %s\n", i2pHostname.c_str());
|
||||
}
|
||||
advertised = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
lastDiscoveryError = e.what();
|
||||
printf("Embedded I2P: server tunnel registry read failed: %s\n", e.what());
|
||||
} catch (...) {
|
||||
lastDiscoveryError = "unknown exception";
|
||||
printf("Embedded I2P: server tunnel registry read failed: "
|
||||
"unknown exception\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (!advertised) {
|
||||
// Not yet in live registry. Leave empty (or clear stale value).
|
||||
if (!i2pHostname.empty()) {
|
||||
printf("Embedded I2P: server tunnel left live registry, "
|
||||
"clearing destination %s\n", i2pHostname.c_str());
|
||||
i2pHostname.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CI2PEmbedded::GetI2PAddress — read the cached destination, retrying
|
||||
// discovery if empty. Called from qt/trianglesgui.cpp:1875
|
||||
// (updateI2PAddress) on every timerI2P tick.
|
||||
//
|
||||
// Threading: read-by-copy under hostnameMutex so concurrent writes by
|
||||
// the bootstrap thread cannot tear the std::string.
|
||||
std::string CI2PEmbedded::GetI2PAddress()
|
||||
{
|
||||
bool needDiscovery = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
needDiscovery = i2pHostname.empty() && running.load();
|
||||
}
|
||||
|
||||
if (needDiscovery) {
|
||||
// Tunnel may have registered since the bootstrap-thread scan.
|
||||
// Re-scans the registry and the keys file (does file I/O); not
|
||||
// "cheap" on retry, but bounded — single registry walk + one
|
||||
// small file read.
|
||||
DiscoverServerTunnelDestination();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
return i2pHostname;
|
||||
}
|
||||
|
||||
#else // !ENABLE_I2P_EMBEDDED
|
||||
|
||||
// ========================================================================
|
||||
@@ -758,6 +994,14 @@ void CI2PEmbedded::Stop()
|
||||
running.store(false);
|
||||
}
|
||||
|
||||
// Stubs for the new methods (header declares them unconditionally)
|
||||
void CI2PEmbedded::DiscoverServerTunnelDestination() {}
|
||||
std::string CI2PEmbedded::GetI2PAddress()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
return i2pHostname;
|
||||
}
|
||||
|
||||
#endif // ENABLE_I2P_EMBEDDED
|
||||
|
||||
// ========================================================================
|
||||
|
||||
+37
-3
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
// Cross-platform socket handle for SAM v3 streaming API.
|
||||
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
|
||||
@@ -92,12 +94,41 @@ private:
|
||||
int samPort; // i2pd SAM bridge port (for SAM v3 protocol)
|
||||
int serverPort; // Triangles P2P listen port (for incoming I2P connections)
|
||||
std::string i2pDataDir; // i2pd data directory (under wallet datadir)
|
||||
// Hostname and discovery-error cache are read by the Qt UI thread
|
||||
// (qt/trianglesgui.cpp:1875 updateI2PAddress) on every 5s timerI2P
|
||||
// tick and written by the bootstrap thread. Mutex-guarded to avoid
|
||||
// a C++ data race on the std::string itself.
|
||||
mutable std::mutex hostnameMutex;
|
||||
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
|
||||
std::string lastError;
|
||||
// I2P bootstrap runs in a background thread; we keep the handle so Stop()
|
||||
// can join it. (A detached thread that is still running blocks process exit.)
|
||||
std::thread routerThread;
|
||||
|
||||
// Server-tunnel destination discovery.
|
||||
//
|
||||
// Scans the live server tunnel registry (i2p::client::context
|
||||
// ::GetServerTunnels()) for an entry whose ident hash matches the
|
||||
// public key in triangles-p2p-keys.dat. Sets i2pHostname to the
|
||||
// corresponding ".b32.i2p" address on success; leaves i2pHostname
|
||||
// empty otherwise. Thread-safe: the registry scan is mutex-guarded
|
||||
// inside libi2pd_client; we only read the resulting map.
|
||||
//
|
||||
// This is a no-op when serverPort == 0 (no inbound server tunnel
|
||||
// configured — pure outbound SOCKS I2P mode).
|
||||
//
|
||||
// Idempotent. Called from the bootstrap thread AND from
|
||||
// GetI2PAddress() when i2pHostname is empty, so the Qt timerI2P
|
||||
// (qt/trianglesgui.cpp:384-387) picks up the result on its next
|
||||
// 5s tick once the tunnel registers.
|
||||
void DiscoverServerTunnelDestination();
|
||||
|
||||
// Cache the most recent discovery failure reason (parsed keys-file
|
||||
// hash, registry-read error, etc.). Visible only to GetStartupError()
|
||||
// callers in the header — no public accessor for lastDiscoveryError
|
||||
// is needed today.
|
||||
std::string lastDiscoveryError;
|
||||
|
||||
public:
|
||||
static CI2PEmbedded* GetInstance();
|
||||
|
||||
@@ -121,8 +152,11 @@ public:
|
||||
int GetServerPort() const { return serverPort; }
|
||||
const std::string& GetDataDir() const { return i2pDataDir; }
|
||||
|
||||
// Get our .b32.i2p destination address
|
||||
std::string GetI2PAddress() const { return i2pHostname; }
|
||||
// Get our .b32.i2p destination address. Triggers a discovery retry
|
||||
// if the hostname is empty (e.g. first attempt raced the tunnel
|
||||
// registration). Idempotent and cheap when the hostname is already
|
||||
// populated.
|
||||
std::string GetI2PAddress();
|
||||
std::string GetStartupError() const { return lastError; }
|
||||
void SetStartupError(const std::string& value) { lastError = value; }
|
||||
|
||||
@@ -144,4 +178,4 @@ public:
|
||||
bool StartEmbeddedI2P();
|
||||
void StopEmbeddedI2P();
|
||||
|
||||
#endif // TRIANGLES_I2P_EMBEDDED_H
|
||||
#endif // TRIANGLES_I2P_EMBEDDED_H
|
||||
+116
-101
@@ -35,6 +35,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
|
||||
// Forward declaration: InitError / InitWarning are defined further down
|
||||
// in this file but referenced by AppInit (line ~423) before the definition.
|
||||
@@ -45,6 +46,12 @@ static bool InitWarning(const std::string& str);
|
||||
#include <algorithm>
|
||||
#include <openssl/crypto.h>
|
||||
|
||||
#ifdef WIN32
|
||||
// _get_osfhandle lives in <io.h>; FlushFileBuffers / HANDLE live in <windows.h>,
|
||||
// which is transitively included via util.h on Windows builds.
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
#ifndef WIN32
|
||||
#include <signal.h>
|
||||
#include <sys/file.h>
|
||||
@@ -102,6 +109,49 @@ bool LockDataDirectory(const std::filesystem::path& pathLockFile)
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SyncReindexMarker(const fs::path& markerPath)
|
||||
{
|
||||
// POSIX systems guarantee parent-directory durability via fsync(dirfd).
|
||||
// Windows does not expose an equivalent primitive for directory metadata;
|
||||
// `_commit` flushes the file's data to disk and the underlying NTFS
|
||||
// journal commits the directory entry on close. Both paths below flush
|
||||
// before close to maximise durability; Windows users get file-data
|
||||
// durability equivalent to POSIX, with directory metadata committed by
|
||||
// the journal.
|
||||
FILE* marker = std::fopen(markerPath.string().c_str(), "wb");
|
||||
if (!marker)
|
||||
return false;
|
||||
static const char text[] = "Reindex must complete successfully before normal startup.\n";
|
||||
bool ok = std::fwrite(text, 1, sizeof(text) - 1, marker) == sizeof(text) - 1 &&
|
||||
std::fflush(marker) == 0;
|
||||
#ifdef WIN32
|
||||
// FlushFileBuffers on the file handle commits data durably to NTFS.
|
||||
intptr_t osHandle = _get_osfhandle(_fileno(marker));
|
||||
if (osHandle == -1 || FlushFileBuffers(reinterpret_cast<HANDLE>(osHandle)) == FALSE)
|
||||
ok = false;
|
||||
#else
|
||||
if (ok)
|
||||
ok = ::fsync(fileno(marker)) == 0;
|
||||
#endif
|
||||
if (std::fclose(marker) != 0)
|
||||
ok = false;
|
||||
#ifdef WIN32
|
||||
// No directory-fsync primitive on Windows. The journal commit on close
|
||||
// (and the FlushFileBuffers above) is the strongest durability available.
|
||||
// See comment block above.
|
||||
#else
|
||||
if (ok)
|
||||
{
|
||||
int dirFd = ::open(markerPath.parent_path().string().c_str(), O_RDONLY | O_DIRECTORY);
|
||||
if (dirFd < 0)
|
||||
return false;
|
||||
ok = ::fsync(dirFd) == 0;
|
||||
::close(dirFd);
|
||||
}
|
||||
#endif
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef WIN32
|
||||
bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
@@ -178,95 +228,19 @@ void ExitTimeout(void* parg)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Wait up to maxWaitSec for at least minPeers peers to have reported their
|
||||
// chain height via the version handshake. Returns the median peer height, or
|
||||
// -1 if we couldn't get enough peers (timeout, no peers, all nStartingHeight=-1).
|
||||
int WaitForPeerHeights(int minPeers, int maxWaitSec)
|
||||
// Automatic recovery is intentionally non-destructive. Older builds deleted the
|
||||
// chain DB and blk0001.dat when a node lagged its peers, which could turn a
|
||||
// transient peer-height report into permanent local data loss. A privacy coin
|
||||
// must never rewrite historical chain data automatically; recovery remains an
|
||||
// explicit operator action after wallet and chain-state backups.
|
||||
// Legacy hook retained only to surface that -autorerebuild no longer mutates
|
||||
// chain state.
|
||||
static void LogAutoRebuildDisabled(int thresholdBlocks)
|
||||
{
|
||||
const int pollIntervalMs = 500;
|
||||
const int64_t deadline = GetTimeMillis() + (int64_t)maxWaitSec * 1000;
|
||||
|
||||
while (GetTimeMillis() < deadline && !fRequestShutdown) {
|
||||
std::vector<int> heights;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode && pnode->nStartingHeight > 0)
|
||||
heights.push_back(pnode->nStartingHeight);
|
||||
}
|
||||
}
|
||||
if ((int)heights.size() >= minPeers) {
|
||||
std::sort(heights.begin(), heights.end());
|
||||
int median = heights[heights.size() / 2];
|
||||
printf("AutoRebuild: got %zu peer heights; median=%d\n", heights.size(), median);
|
||||
return median;
|
||||
}
|
||||
MilliSleep(pollIntervalMs);
|
||||
if (thresholdBlocks > 0) {
|
||||
printf("AutoRebuild: -autorerebuild=%d ignored; automatic chain deletion is disabled.\n",
|
||||
thresholdBlocks);
|
||||
}
|
||||
|
||||
std::vector<int> heights;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode && pnode->nStartingHeight > 0)
|
||||
heights.push_back(pnode->nStartingHeight);
|
||||
}
|
||||
}
|
||||
if (heights.empty()) {
|
||||
printf("AutoRebuild: no peers reported heights after %ds\n", maxWaitSec);
|
||||
return -1;
|
||||
}
|
||||
std::sort(heights.begin(), heights.end());
|
||||
int median = heights[heights.size() / 2];
|
||||
printf("AutoRebuild: timed out with %zu peers; median=%d\n", heights.size(), median);
|
||||
return median;
|
||||
}
|
||||
|
||||
// If -autorerebuild is set and our local chain is more than that many blocks
|
||||
// behind the median peer height, wipe the chain DB (preserving wallet.dat +
|
||||
// onion + smsg state) and request shutdown. On restart, the daemon sees no
|
||||
// chain DB and the snapshot path takes over.
|
||||
void MaybeAutoRebuild(int thresholdBlocks)
|
||||
{
|
||||
if (thresholdBlocks <= 0)
|
||||
return;
|
||||
|
||||
if (nBestHeight < 0) {
|
||||
printf("AutoRebuild: local nBestHeight unset — skipping\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("AutoRebuild: enabled (threshold=%d blocks). Local chain tip: %d\n",
|
||||
thresholdBlocks, nBestHeight);
|
||||
int medianPeer = WaitForPeerHeights(/*minPeers=*/3, /*maxWaitSec=*/60);
|
||||
if (medianPeer <= 0) {
|
||||
printf("AutoRebuild: could not get peer heights — skipping rebuild\n");
|
||||
return;
|
||||
}
|
||||
|
||||
int lag = medianPeer - nBestHeight;
|
||||
printf("AutoRebuild: peer median=%d, local=%d, lag=%d\n",
|
||||
medianPeer, nBestHeight, lag);
|
||||
|
||||
if (lag < thresholdBlocks) {
|
||||
printf("AutoRebuild: lag %d < threshold %d — no rebuild needed\n",
|
||||
lag, thresholdBlocks);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n*** AutoRebuild: chain is %d blocks behind — wiping chain DB ***\n", lag);
|
||||
printf("*** Preserving wallet.dat, smsgDB, onion state. ***\n");
|
||||
printf("*** Daemon will shutdown; restart to load signed UTXO snapshot. ***\n\n");
|
||||
|
||||
WipeChainDataDir();
|
||||
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (fs::exists(blkPath)) {
|
||||
fs::remove(blkPath);
|
||||
printf("AutoRebuild: removed stale %s\n", blkPath.string().c_str());
|
||||
}
|
||||
|
||||
StartShutdown();
|
||||
}
|
||||
|
||||
void StartShutdown()
|
||||
@@ -655,7 +629,7 @@ std::string HelpMessage()
|
||||
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
|
||||
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
|
||||
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
|
||||
" -autorerebuild=<n> " + _("If our chain is more than <n> blocks behind peers, wipe chain DB and shutdown for clean restart (default: 0=disabled)") + "\n" +
|
||||
" -autorerebuild=<n> " + _("Deprecated compatibility option; automatic chain deletion is disabled") + "\n" +
|
||||
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
|
||||
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
|
||||
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
|
||||
@@ -707,7 +681,8 @@ std::string HelpMessage()
|
||||
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
|
||||
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
|
||||
" -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" +
|
||||
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
|
||||
" -reindex " + _("Rebuild the derived chain database from the existing blk0001.dat without modifying the raw block file") + "\n" +
|
||||
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
|
||||
|
||||
"\n" + _("Block creation options:") + "\n" +
|
||||
" -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" +
|
||||
@@ -1424,22 +1399,66 @@ bool AppInit2()
|
||||
}
|
||||
|
||||
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
|
||||
// blk*.dat files. This recalculates money
|
||||
// blk0001.dat file used by this storage format. This recalculates money
|
||||
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
|
||||
// WipeChainDataDir(), which resolves the directory per the configured
|
||||
// -chaindb backend.
|
||||
if (GetBoolArg("-reindex", false))
|
||||
const bool fReindex = GetBoolArg("-reindex", false);
|
||||
fs::path reindexMarker = GetDataDir() / "REINDEX_INCOMPLETE";
|
||||
|
||||
// Validate the immutable source before removing any derived state. A marker
|
||||
// survives crashes/interruption so ordinary startup cannot trust a partial
|
||||
// database left by an earlier recovery attempt.
|
||||
if (fReindex)
|
||||
{
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath) || !fs::is_regular_file(blkPath))
|
||||
return InitError(_("Reindex requested but blk0001.dat is missing or not a regular file"));
|
||||
if (!SyncReindexMarker(reindexMarker))
|
||||
return InitError(_("Cannot durably create REINDEX_INCOMPLETE marker in the data directory"));
|
||||
|
||||
printf("Reindex requested: removing chain database...\n");
|
||||
uiInterface.InitMessage(_("Removing chain database for reindex..."));
|
||||
WipeChainDataDir();
|
||||
if (fs::exists(GetChainDataDir()))
|
||||
return InitError(_("Reindex could not remove the existing chain database"));
|
||||
}
|
||||
else if (fs::exists(reindexMarker))
|
||||
{
|
||||
return InitError(_("A previous reindex was interrupted. Restart with -reindex to rebuild derived chain state."));
|
||||
}
|
||||
|
||||
uiInterface.InitMessage(_("Loading block index..."));
|
||||
printf("Loading block index...\n");
|
||||
nStart = GetTimeMillis();
|
||||
if (!LoadBlockIndex())
|
||||
// Normal startup loads the existing derived index. An explicit -reindex
|
||||
// must NOT call LoadBlockIndex() first: on an empty database that routine
|
||||
// creates and appends a new genesis record to blk0001.dat. Reindex instead
|
||||
// rebuilds directly from the already-existing raw history, keeping the
|
||||
// source block file byte-for-byte unchanged.
|
||||
if (fReindex)
|
||||
{
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath))
|
||||
return InitError(_("Reindex requested but blk0001.dat is missing"));
|
||||
|
||||
printf("Reindex: rebuilding chain database from existing %s (raw block file will not be modified)\n",
|
||||
blkPath.string().c_str());
|
||||
uiInterface.InitMessage(_("Reindexing blocks from blk0001.dat..."));
|
||||
int64_t nReindexStart = GetTimeMillis();
|
||||
if (!FastImportBlockFile())
|
||||
return InitError(_("Reindex failed while rebuilding from blk0001.dat"));
|
||||
StartupPerfLog("reindex_fast_import", GetTimeMillis() - nReindexStart,
|
||||
strprintf("bestheight=%d indexsize=%" PRIszu,
|
||||
nBestHeight, mapBlockIndex.size()));
|
||||
std::error_code markerError;
|
||||
if (!fs::remove(reindexMarker, markerError) || markerError)
|
||||
return InitError(_("Reindex completed but REINDEX_INCOMPLETE marker could not be removed"));
|
||||
}
|
||||
else if (!LoadBlockIndex())
|
||||
{
|
||||
return InitError(_("Error loading blkindex.dat"));
|
||||
}
|
||||
|
||||
// pindexLastHardenedCheckpoint is initialized from the hardened checkpoint
|
||||
// map on startup, BEFORE the daemon opens any peer connections or
|
||||
@@ -1581,16 +1600,12 @@ bool AppInit2()
|
||||
uiInterface.InitMessage(_("UTXO rebuild complete"));
|
||||
}
|
||||
|
||||
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
|
||||
// and shutdown for clean restart.
|
||||
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
|
||||
if (fRequestShutdown) {
|
||||
printf("AutoRebuild: shutdown requested before chain load complete\n");
|
||||
return false;
|
||||
}
|
||||
// Keep the legacy option parse for compatibility, but automatic recovery is
|
||||
// diagnostic-only and never removes chain data.
|
||||
LogAutoRebuildDisabled(GetArg("-autorerebuild", 0));
|
||||
|
||||
// Block index loaded. With fast-import removed, the only supported sync path
|
||||
// is the UTXO snapshot (auto-downloaded from bootstrap or placed manually in datadir).
|
||||
// Block index loaded. Normal bootstrap uses the UTXO snapshot; explicit
|
||||
// -reindex is the operator-only recovery path from local blk0001.dat.
|
||||
|
||||
// as LoadBlockIndex can take several minutes, it's possible the user
|
||||
// requested to kill triangles-qt during the last operation. If so, exit.
|
||||
|
||||
+390
-88
@@ -27,6 +27,7 @@
|
||||
#include <memory>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
|
||||
|
||||
using namespace std;
|
||||
@@ -118,12 +119,12 @@ void ThreadForkDetector(void*)
|
||||
printf("*** Possible fork or sync stall. Check peers: 'getpeerinfo' and chain: 'getblockhash %d' ***\n",
|
||||
nOurHeight);
|
||||
|
||||
// If severe lag persists, suggest auto-rebuild
|
||||
if (lag >= threshold * 3 && GetBoolArg("-autorerebuild", 0) > 0)
|
||||
// Severe lag is diagnostic only. Recovery must be explicitly
|
||||
// initiated by an operator after backups; never request an
|
||||
// automatic shutdown that could lead to chain-state deletion.
|
||||
if (lag >= threshold * 3 && GetBoolArg("-autorerebuild", false))
|
||||
{
|
||||
printf("*** FORK DETECTOR: lag %d >= %d, triggering AutoRebuild ***\n",
|
||||
lag, threshold * 3);
|
||||
StartShutdown();
|
||||
printf("*** FORK DETECTOR: automatic rebuild is disabled; operator review required ***\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4145,88 +4146,213 @@ bool LoadExternalBlockFile(FILE* fileIn)
|
||||
|
||||
bool FastImportBlockFile()
|
||||
{
|
||||
// Fast block import: reads blk0001.dat and builds the block index
|
||||
// directly without re-writing block data. LevelDB writes are batched
|
||||
// every 200K blocks for speed. Only used for trusted bootstrap data
|
||||
// (blocks below the hardcoded checkpoint).
|
||||
// Explicit recovery importer: read the single raw block file used by this
|
||||
// storage format and reconstruct all derived chain state without writing
|
||||
// to blk0001.dat. The caller gates this behind -reindex.
|
||||
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath))
|
||||
return false;
|
||||
|
||||
// LoadBlockIndex normally initializes these before opening the database.
|
||||
// Reindex bypasses its genesis-creation path, so initialize the same
|
||||
// network-specific framing and consensus parameters here.
|
||||
if (fTestNet)
|
||||
{
|
||||
pchMessageStart[0] = 0x6f;
|
||||
pchMessageStart[1] = 0x3e;
|
||||
pchMessageStart[2] = 0x04;
|
||||
pchMessageStart[3] = 0x13;
|
||||
bnProofOfStakeLimit = bnProofOfStakeLimitTestNet;
|
||||
bnProofOfWorkLimit = bnProofOfWorkLimitTestNet;
|
||||
nStakeMinAge = 10 * 60;
|
||||
nStakeMaxAge = 30 * 60;
|
||||
nModifierInterval = 60;
|
||||
nCoinbaseMaturity = 10;
|
||||
nTargetSpacing = 60;
|
||||
}
|
||||
|
||||
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
|
||||
int64_t nStart = GetTimeMillis();
|
||||
|
||||
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
|
||||
if (!fileIn)
|
||||
return false;
|
||||
std::unique_ptr<FILE, int(*)(FILE*)> fileGuard(fileIn, &fclose);
|
||||
|
||||
// Get file size for progress
|
||||
fseek(fileIn, 0, SEEK_END);
|
||||
if (fseek(fileIn, 0, SEEK_END) != 0)
|
||||
return error("FastImportBlockFile: cannot seek to end of blk0001.dat");
|
||||
int64_t nFileSize = ftell(fileIn);
|
||||
fseek(fileIn, 0, SEEK_SET);
|
||||
if (nFileSize <= 0 || nFileSize > (int64_t)std::numeric_limits<unsigned int>::max() ||
|
||||
fseek(fileIn, 0, SEEK_SET) != 0)
|
||||
return error("FastImportBlockFile: blk0001.dat size is invalid or exceeds the 32-bit disk-position format");
|
||||
|
||||
int nLoaded = 0;
|
||||
int64_t nLastProgressReport = 0;
|
||||
int nRootBlocks = 0;
|
||||
int64_t nLastRecordEnd = 0;
|
||||
const uint256 expectedGenesis = fTestNet ? hashGenesisBlockTestNet : hashGenesisBlockOfficial;
|
||||
|
||||
{
|
||||
LOCK(cs_main);
|
||||
CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
txdb.TxnBegin();
|
||||
auto txdb_holder = MakeChainDB("cr+"); CTxDBBase& txdb = *txdb_holder;
|
||||
if (!txdb.TxnBegin())
|
||||
return error("FastImportBlockFile: failed to begin database transaction");
|
||||
|
||||
unsigned int nPos = 0;
|
||||
while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown)
|
||||
try
|
||||
{
|
||||
// Find message start bytes (same scan as LoadExternalBlockFile)
|
||||
unsigned char pchData[65536];
|
||||
do {
|
||||
fseek(blkdat, nPos, SEEK_SET);
|
||||
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
|
||||
if (nRead <= 8)
|
||||
{
|
||||
nPos = (unsigned int)-1;
|
||||
break;
|
||||
}
|
||||
void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart));
|
||||
if (nFind)
|
||||
{
|
||||
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0)
|
||||
{
|
||||
nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart);
|
||||
break;
|
||||
}
|
||||
nPos += ((unsigned char*)nFind - pchData) + 1;
|
||||
}
|
||||
else
|
||||
nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1;
|
||||
} while(!fRequestShutdown);
|
||||
// The entire import runs inside this try block. The catch below
|
||||
// guarantees the in-flight transaction is explicitly aborted on
|
||||
// any exception (allocation, database, validation, or otherwise)
|
||||
// before propagating, so a partial commit cannot leak even if the
|
||||
// inner error paths miss a TxnAbort. Each inner error path also
|
||||
// aborts explicitly for clarity.
|
||||
|
||||
if (nPos == (unsigned int)-1)
|
||||
break;
|
||||
|
||||
fseek(blkdat, nPos, SEEK_SET);
|
||||
unsigned int nSize;
|
||||
blkdat >> nSize;
|
||||
|
||||
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
||||
unsigned int nPos = 0;
|
||||
while ((int64_t)nPos < nFileSize && !fRequestShutdown)
|
||||
{
|
||||
// Strict contiguous framing: every record must begin exactly at
|
||||
// nPos with network magic + declared payload size. Do not scan
|
||||
// forward through garbage; recovery must prove the whole file.
|
||||
if (nFileSize - nPos < (int64_t)(sizeof(pchMessageStart) + sizeof(uint32_t)))
|
||||
{
|
||||
nPos += 4 + nSize;
|
||||
continue;
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: truncated record header at file offset %u", nPos);
|
||||
}
|
||||
unsigned char recordMagic[sizeof(pchMessageStart)];
|
||||
if (fseek(fileIn, nPos, SEEK_SET) != 0 ||
|
||||
fread(recordMagic, 1, sizeof(recordMagic), fileIn) != sizeof(recordMagic) ||
|
||||
memcmp(recordMagic, pchMessageStart, sizeof(recordMagic)) != 0)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: invalid record magic at file offset %u", nPos);
|
||||
}
|
||||
|
||||
// nBlockPos = file position where the block data starts
|
||||
// (after 4-byte message start + 4-byte size)
|
||||
unsigned int nBlockPos = nPos + 4;
|
||||
uint32_t nSize = 0;
|
||||
if (fread(&nSize, sizeof(nSize), 1, fileIn) != 1)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: cannot read block size at file offset %u", nPos);
|
||||
}
|
||||
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: invalid block size %u at file offset %u", nSize, nPos);
|
||||
}
|
||||
const int64_t payloadPos = (int64_t)nPos + sizeof(pchMessageStart) + sizeof(nSize);
|
||||
if (payloadPos + nSize > nFileSize)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: truncated block record at file offset %u", nPos);
|
||||
}
|
||||
|
||||
std::vector<char> payload(nSize);
|
||||
if (fread(payload.data(), 1, nSize, fileIn) != nSize)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: short payload read at file offset %u", nPos);
|
||||
}
|
||||
|
||||
const unsigned int nBlockPos = (unsigned int)payloadPos;
|
||||
CBlock block;
|
||||
blkdat >> block;
|
||||
try
|
||||
{
|
||||
CDataStream record(payload.data(), payload.data() + payload.size(),
|
||||
SER_DISK, CLIENT_VERSION);
|
||||
record >> block;
|
||||
if (!record.empty())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: block payload has %" PRIszu " trailing bytes at file offset %u",
|
||||
record.size(), nPos);
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: malformed block payload at file offset %u: %s",
|
||||
nPos, e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: unknown deserialization failure at file offset %u",
|
||||
nPos);
|
||||
}
|
||||
|
||||
const unsigned int nRecordEnd = (unsigned int)(payloadPos + nSize);
|
||||
|
||||
// Reindex is optimized for trusted local history but must still
|
||||
// apply every context-free block/transaction invariant before it
|
||||
// can write derived state. Context-dependent chain validity is
|
||||
// anchored below by exact genesis, parent continuity, cumulative
|
||||
// trust selection, and all compiled hardened checkpoints.
|
||||
//
|
||||
// PoS block-signature verification follows the runtime rule:
|
||||
// - blocks above the newest compiled checkpoint must be
|
||||
// individually signed and chain-trust valid;
|
||||
// - blocks at or below the newest compiled checkpoint are
|
||||
// covered by the historical assume-valid fast path, which
|
||||
// is the same rule the daemon uses at runtime. We must NOT
|
||||
// apply the per-block signature check unconditionally,
|
||||
// because that policy change was deliberately added in
|
||||
// v6.x to prevent chain splits over the pre-checkpoint era.
|
||||
if (!block.CheckBlock(true, true, false))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: block failed context-free validation at file offset %u",
|
||||
nPos);
|
||||
}
|
||||
if (block.IsProofOfStake() && pindexBest->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
||||
!block.CheckBlockSignature())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: post-checkpoint block signature failure at file offset %u",
|
||||
nPos);
|
||||
}
|
||||
|
||||
uint256 hash = block.GetHash();
|
||||
if (block.hashPrevBlock == 0)
|
||||
{
|
||||
// The expected network genesis is the very first record in the
|
||||
// file (offset 0). The runtime rule is "blocks whose parent is
|
||||
// zero are only the genesis", and any other record with a zero
|
||||
// parent would corrupt the active chain, so reject anything
|
||||
// that hashes to the genesis hash anywhere other than offset 0.
|
||||
++nRootBlocks;
|
||||
if (hash == expectedGenesis)
|
||||
{
|
||||
if (nPos != 0 || nRootBlocks != 1)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: unexpected or duplicate genesis block %s at file offset %u",
|
||||
hash.ToString().c_str(), nPos);
|
||||
}
|
||||
}
|
||||
else if (nPos == 0)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: first record is not the expected genesis block %s",
|
||||
hash.ToString().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stray root record (previous broken -reindex runs may have
|
||||
// appended a fresh genesis record to blk0001.dat). Skip it:
|
||||
// it has no parent, no chain trust, and would otherwise be
|
||||
// a false duplicate of genesis. Advance strictly so the
|
||||
// exact-file-consumed invariant still holds.
|
||||
nPos = nRecordEnd;
|
||||
nLastRecordEnd = nPos;
|
||||
nLoaded++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (mapBlockIndex.count(hash))
|
||||
{
|
||||
nPos += 4 + nSize;
|
||||
nPos = nRecordEnd;
|
||||
nLastRecordEnd = nPos;
|
||||
continue; // already indexed
|
||||
}
|
||||
|
||||
@@ -4242,6 +4368,31 @@ bool FastImportBlockFile()
|
||||
pindexNew->pprev = miPrev->second;
|
||||
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
||||
}
|
||||
else if (block.hashPrevBlock != 0)
|
||||
{
|
||||
delete pindexNew;
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: parent %s missing before block %s",
|
||||
block.hashPrevBlock.ToString().c_str(), hash.ToString().c_str());
|
||||
}
|
||||
|
||||
if (!Checkpoints::CheckHardened(pindexNew->nHeight, hash))
|
||||
{
|
||||
const int badHeight = pindexNew->nHeight;
|
||||
delete pindexNew;
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: hardened checkpoint mismatch at height %d",
|
||||
badHeight);
|
||||
}
|
||||
if (pindexNew->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
||||
!block.CheckBlockSignature())
|
||||
{
|
||||
const int badHeight = pindexNew->nHeight;
|
||||
delete pindexNew;
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: post-checkpoint block signature failure at height %d",
|
||||
badHeight);
|
||||
}
|
||||
|
||||
// Chain trust
|
||||
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
||||
@@ -4249,19 +4400,24 @@ bool FastImportBlockFile()
|
||||
// Stake entropy bit
|
||||
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
|
||||
|
||||
// Stake modifier (minimal for blocks far below checkpoint)
|
||||
int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
||||
if (pindexNew->nHeight >= nCheckpointHeight - 1000)
|
||||
// Recompute the exact historical stake-modifier chain. Every
|
||||
// block must participate: using placeholder zero modifiers for
|
||||
// older blocks leaves mature wallet UTXOs unable to resolve the
|
||||
// later modifier required by CheckStakeKernelHash(). Reindex is
|
||||
// an explicit recovery operation, so correctness takes priority
|
||||
// over the old shortcut's speed.
|
||||
uint64_t nStakeModifier = 0;
|
||||
bool fGeneratedStakeModifier = false;
|
||||
if (!ComputeNextStakeModifier(pindexNew->pprev,
|
||||
nStakeModifier,
|
||||
fGeneratedStakeModifier))
|
||||
{
|
||||
uint64_t nStakeModifier = 0;
|
||||
bool fGeneratedStakeModifier = false;
|
||||
ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier);
|
||||
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0);
|
||||
delete pindexNew;
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to compute stake modifier for block %s",
|
||||
hash.ToString().c_str());
|
||||
}
|
||||
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
||||
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
||||
|
||||
// PoS stake seen set
|
||||
@@ -4272,9 +4428,9 @@ bool FastImportBlockFile()
|
||||
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
|
||||
pindexNew->phashBlock = &mi->first;
|
||||
|
||||
// Link pnext for previous block
|
||||
if (pindexNew->pprev)
|
||||
pindexNew->pprev->pnext = pindexNew;
|
||||
// pnext is rebuilt after best-chain selection. File order also
|
||||
// contains side branches, so assigning it here would let the last
|
||||
// imported child hijack stake-modifier forward walks.
|
||||
|
||||
// NOTE: tx-index, UTXO-set and money-supply application are
|
||||
// DEFERRED to a second pass over the active (best-trust) chain
|
||||
@@ -4285,7 +4441,12 @@ bool FastImportBlockFile()
|
||||
// That was the root cause of UTXO-set / supply inflation on every
|
||||
// reindex. Here we only build the block index for all blocks so
|
||||
// best-chain selection by trust still works.
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
||||
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew)))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to write block index %s",
|
||||
hash.ToString().c_str());
|
||||
}
|
||||
|
||||
// Update best chain
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
@@ -4303,14 +4464,27 @@ bool FastImportBlockFile()
|
||||
pindexGenesisBlock = pindexNew;
|
||||
|
||||
nLoaded++;
|
||||
nPos += 4 + nSize;
|
||||
nPos = nRecordEnd;
|
||||
nLastRecordEnd = nPos;
|
||||
|
||||
// Batch commit every 200K blocks for LevelDB efficiency
|
||||
if (nLoaded % 200000 == 0)
|
||||
{
|
||||
txdb.WriteHashBestChain(hashBestChain);
|
||||
txdb.TxnCommit();
|
||||
txdb.TxnBegin();
|
||||
if (!txdb.WriteHashBestChain(hashBestChain))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: index batch WriteHashBestChain failed after %d blocks", nLoaded);
|
||||
}
|
||||
if (!txdb.TxnCommit())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: index batch TxnCommit failed after %d blocks", nLoaded);
|
||||
}
|
||||
if (!txdb.TxnBegin())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: index batch TxnBegin failed after %d blocks", nLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
// Report progress every 5000 blocks to keep GUI responsive.
|
||||
@@ -4324,6 +4498,42 @@ bool FastImportBlockFile()
|
||||
}
|
||||
}
|
||||
|
||||
if (fRequestShutdown)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: interrupted after %d blocks; reindex is incomplete", nLoaded);
|
||||
}
|
||||
if (nRootBlocks < 1 || nLastRecordEnd != nFileSize)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: block file was not consumed exactly (roots=%d end=%" PRId64 " size=%" PRId64 ")",
|
||||
nRootBlocks, nLastRecordEnd, nFileSize);
|
||||
}
|
||||
if (!pindexBest || !pindexGenesisBlock ||
|
||||
pindexGenesisBlock->GetBlockHash() != expectedGenesis)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: no complete active chain found");
|
||||
}
|
||||
|
||||
const int requiredCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
|
||||
CBlockIndex* requiredCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||
if (requiredCheckpointHeight < 0 || !requiredCheckpoint ||
|
||||
requiredCheckpoint->nHeight != requiredCheckpointHeight)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: selected chain does not reach the newest compiled checkpoint at height %d",
|
||||
requiredCheckpointHeight);
|
||||
}
|
||||
CBlockIndex* checkpointAncestor = pindexBest;
|
||||
while (checkpointAncestor && checkpointAncestor->nHeight > requiredCheckpointHeight)
|
||||
checkpointAncestor = checkpointAncestor->pprev;
|
||||
if (checkpointAncestor != requiredCheckpoint)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: newest compiled checkpoint is not on selected active chain");
|
||||
}
|
||||
|
||||
// ---- Pass 2: apply tx-index, UTXO set and money supply along the
|
||||
// ACTIVE (best-trust) chain ONLY. The file-order pass above indexed
|
||||
// every block including orphaned side-chain blocks; replaying only
|
||||
@@ -4335,6 +4545,15 @@ bool FastImportBlockFile()
|
||||
for (CBlockIndex* p = pindexBest; p; p = p->pprev)
|
||||
vMain.push_back(p);
|
||||
std::reverse(vMain.begin(), vMain.end());
|
||||
|
||||
// File order includes side branches. Build pnext exclusively from
|
||||
// the selected best-trust chain so kernel-modifier forward walks
|
||||
// cannot follow whichever side-chain child appeared last.
|
||||
for (const auto& item : mapBlockIndex)
|
||||
item.second->pnext = nullptr;
|
||||
for (size_t i = 1; i < vMain.size(); ++i)
|
||||
vMain[i - 1]->pnext = vMain[i];
|
||||
|
||||
printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size());
|
||||
uiInterface.InitMessage(_("Building UTXO set (main chain)..."));
|
||||
|
||||
@@ -4342,6 +4561,13 @@ bool FastImportBlockFile()
|
||||
int nApplied = 0;
|
||||
for (CBlockIndex* pindex : vMain)
|
||||
{
|
||||
if (fRequestShutdown)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: interrupted during active-chain replay at height %d",
|
||||
pindex->nHeight);
|
||||
}
|
||||
|
||||
// Genesis (height 0) is a hardcoded special block that is not
|
||||
// re-read from disk this way; it contributes nothing to supply
|
||||
// and the genesis-walk audit skips it identically. Carry the
|
||||
@@ -4350,13 +4576,20 @@ bool FastImportBlockFile()
|
||||
{
|
||||
pindex->nMint = 0;
|
||||
pindex->nMoneySupply = nRunningSupply; // still 0 here
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
||||
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to write genesis index");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
CBlock blockMain;
|
||||
if (!blockMain.ReadFromDisk(pindex))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight);
|
||||
}
|
||||
|
||||
int64_t nBlockValueIn = 0;
|
||||
int64_t nBlockValueOut = 0;
|
||||
@@ -4366,7 +4599,12 @@ bool FastImportBlockFile()
|
||||
{
|
||||
uint256 hashTx = tx.GetHash();
|
||||
CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2);
|
||||
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
|
||||
if (!txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size())))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to write txindex %s",
|
||||
hashTx.ToString().c_str());
|
||||
}
|
||||
nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
nBlockValueOut += tx.GetValueOut();
|
||||
@@ -4375,9 +4613,20 @@ bool FastImportBlockFile()
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
CUtxoEntry uprev;
|
||||
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
||||
nBlockValueIn += uprev.nValue;
|
||||
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
|
||||
if (!txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: missing spent UTXO %s:%u at height %d",
|
||||
txin.prevout.hash.ToString().c_str(), txin.prevout.n,
|
||||
pindex->nHeight);
|
||||
}
|
||||
nBlockValueIn += uprev.nValue;
|
||||
if (!txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to erase spent UTXO %s:%u",
|
||||
txin.prevout.hash.ToString().c_str(), txin.prevout.n);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||
@@ -4391,16 +4640,40 @@ bool FastImportBlockFile()
|
||||
utxo.fCoinBase = tx.IsCoinBase();
|
||||
utxo.fCoinStake = tx.IsCoinStake();
|
||||
utxo.nTxTime = tx.nTime;
|
||||
txdb.WriteUtxo(hashTx, k, utxo);
|
||||
if (!txdb.WriteUtxo(hashTx, k, utxo))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to write UTXO %s:%u",
|
||||
hashTx.ToString().c_str(), k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pindex->nMint = nBlockValueOut - nBlockValueIn;
|
||||
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
||||
pindex->nMoneySupply = nRunningSupply;
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
||||
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to update active block index at height %d",
|
||||
pindex->nHeight);
|
||||
}
|
||||
|
||||
if (++nApplied % 200000 == 0) { txdb.TxnCommit(); txdb.TxnBegin(); }
|
||||
if (++nApplied % 200000 == 0)
|
||||
{
|
||||
if (!txdb.TxnCommit())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: active-chain batch TxnCommit failed at height %d",
|
||||
pindex->nHeight);
|
||||
}
|
||||
if (!txdb.TxnBegin())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: active-chain batch TxnBegin failed at height %d",
|
||||
pindex->nHeight);
|
||||
}
|
||||
}
|
||||
if (nApplied % 5000 == 0)
|
||||
{
|
||||
int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size()));
|
||||
@@ -4411,14 +4684,43 @@ bool FastImportBlockFile()
|
||||
}
|
||||
|
||||
// Final commit
|
||||
if (pindexBest)
|
||||
if (fRequestShutdown)
|
||||
{
|
||||
txdb.WriteHashBestChain(hashBestChain);
|
||||
|
||||
// Write sync checkpoint
|
||||
Checkpoints::WriteSyncCheckpoint(hashBestChain);
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: interrupted before final commit");
|
||||
}
|
||||
if (!txdb.WriteHashBestChain(hashBestChain))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to persist best-chain hash");
|
||||
}
|
||||
|
||||
// Write sync checkpoint
|
||||
if (!Checkpoints::WriteSyncCheckpoint(hashBestChain))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: failed to persist sync checkpoint");
|
||||
}
|
||||
|
||||
if (!txdb.TxnCommit())
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: final database commit failed");
|
||||
}
|
||||
} // end try { ... FastImportBlockFile inner LOCK }
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
// Any exception escaping the import (allocation failure, database
|
||||
// throw, unexpected validation throw) MUST NOT leak a partial
|
||||
// commit. Abort the in-flight transaction before propagating.
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: uncaught exception during import: %s", e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("FastImportBlockFile: unknown exception during import");
|
||||
}
|
||||
txdb.TxnCommit();
|
||||
}
|
||||
|
||||
nTransactionsUpdated++;
|
||||
|
||||
@@ -141,6 +141,7 @@ CBlockIndex* FindBlockByHeight(int nHeight);
|
||||
bool ProcessMessages(CNode* pfrom);
|
||||
bool SendMessages(CNode* pto, bool fSendTrickle);
|
||||
bool LoadExternalBlockFile(FILE* fileIn);
|
||||
bool FastImportBlockFile();
|
||||
|
||||
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
|
||||
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
|
||||
|
||||
@@ -873,4 +873,46 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
||||
"trusting its PASS.");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(reindex_reconstruction_is_explicit_and_fail_closed)
|
||||
{
|
||||
// Pin the startup bridge and the fail-closed invariants structurally. The
|
||||
// end-to-end test separately reconstructs the production blk0001.dat;
|
||||
// these checks prevent a refactor from silently returning to the old
|
||||
// "wipe DB, create genesis, never import" behavior.
|
||||
const std::filesystem::path here(__FILE__);
|
||||
const std::filesystem::path root = here.parent_path().parent_path().parent_path();
|
||||
|
||||
std::ifstream initFile(root / "src" / "init.cpp");
|
||||
std::ifstream mainFile(root / "src" / "main.cpp");
|
||||
BOOST_REQUIRE(initFile.good());
|
||||
BOOST_REQUIRE(mainFile.good());
|
||||
|
||||
const std::string initSrc((std::istreambuf_iterator<char>(initFile)),
|
||||
std::istreambuf_iterator<char>());
|
||||
const std::string mainSrc((std::istreambuf_iterator<char>(mainFile)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
BOOST_CHECK(initSrc.find("const bool fReindex = GetBoolArg(\"-reindex\", false)") != std::string::npos);
|
||||
BOOST_CHECK(initSrc.find("if (!FastImportBlockFile())") != std::string::npos);
|
||||
BOOST_CHECK(initSrc.find("else if (!LoadBlockIndex())") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("if (fRequestShutdown)") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("reindex is incomplete") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("interrupted during active-chain replay") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("unexpected or duplicate genesis block") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("invalid record magic") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("malformed block payload") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("trailing bytes at file offset") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("block file was not consumed exactly") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("block failed context-free validation") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("hardened checkpoint mismatch") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("selected chain does not reach the newest compiled checkpoint") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("nFileSize > (int64_t)std::numeric_limits") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("std::unique_ptr<FILE") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("item.second->pnext = nullptr") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("failed to persist best-chain hash") != std::string::npos);
|
||||
BOOST_CHECK(mainSrc.find("final database commit failed") != std::string::npos);
|
||||
BOOST_CHECK(initSrc.find("REINDEX_INCOMPLETE") != std::string::npos);
|
||||
BOOST_CHECK(initSrc.find("SyncReindexMarker") != std::string::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user