Compare commits

...

11 Commits

Author SHA1 Message Date
Hermes Agent 66ac7e8537 chore: bump version to v6.2.6.2 (cycle-25 I2P server-tunnel-destination fix)
Refs: urn:ump:2hqnyywnaxqypxmlts2afklzw6xk4vdolspc3fswdlfvzc3j6tlq
Includes: 93f8795 (i2p_embedded.cpp fix)
2026-08-05 20:19:34 -07:00
Hermes Agent 93f879583f [grade=B urn:ump:2hqnyywnaxqypxmlts2afklzw6xk4vdolspc3fswdlfvzc3j6tlq] fix(i2p): advertise server-tunnel destination, not router identity
The embedded i2p_embedded.cpp used to set i2pHostname from
i2p::context.GetRouterInfo().GetIdentHash() — the embedded router's
own identity. But the Triangles P2P layer listens on a server tunnel
loaded from triangles-p2p-keys.dat, which has a SEPARATE identity.

Inbound I2P peers that dial the advertised router-identity address
fail with SOCKS code 4 / LeaseSet not found, because no LeaseSet for
the router identity is ever published.

Replace the 3-line snippet by a 110-line fix that:

  1. Reads triangles-p2p-keys.dat directly and parses it via
     i2p::data::PrivateKeys::FromBuffer (binary blob format, length
     matches PrivateKeys::GetFullLen()). Extracts the destination
     ident hash from the public key.

  2. Cross-checks against i2p::client::context.GetServerTunnels()
     (map<pair<IdentHash,int>, shared_ptr<I2PServerTunnel>>). If a
     registered tunnel matches the keys-file hash, use that
     destination.

  3. Falls back to publishing the keys-file hash directly if no
     tunnel has registered yet (race during the same startup pass —
     the keys file is the source of truth either way).

  4. Fails CLOSED (clears i2pHostname) if both paths fail, rather
     than silently falling back to the router identity — that
     fallback WAS the bug.

Verified:
- Build: cmake --build succeeds, trianglesd SHA
  0b2affeb6cf86cc0e58620abf8dbfe812091d114bf2497b9d40b0e536b30bc77
- Tests: 291/291 unit tests pass, including 25/25 consensus_safety_tests
- API: libi2pd PrivateKeys::FromBuffer, GetPublic(), GetIdentHash();
  libi2pd_client::context.GetServerTunnels() — all match the bundled
  i2pd 2.60.0 source
- Codex grade B (no blocking issues; 3 polish suggestions addressed)

Polished per Codex B:
- Line 738-742: comment corrected FromBase64 -> FromBuffer + softened
  reachability claim (registry confirms registration, not LeaseSet
  reachability)
- Line 798-800: same reachability softening
- The deliberate non-strict FromBuffer check (accept any nonzero
  return) is preserved because it mirrors i2pd's own loader behavior
  in libi2pd_client/ClientContext.cpp:285-313

Refs: urn:ump:2hqnyywnaxqypxmlts2afklzw6xk4vdolspc3fswdlfvzc3j6tlq
Fixes cycle-25 zero-I2P-peer root cause diagnosed in cycle-24.
2026-08-05 20:18:44 -07:00
Krystie b13139ad19 [grade=B urn:ump:7xuqv7qt7yyvchfbwdciklpoeh2cptk4czo7637allvx45zx6rlq] fix(test): pin WORKING_DIRECTORY to CMAKE_SOURCE_DIR for triangles_unit_tests
The consensus_safety_tests reindex_reconstruction_is_explicit_and_fail_closed
test reads src/init.cpp + src/main.cpp via __FILE__-relative 3x parent_path()
traversal. CI runs 'cd build/src && ctest', so the default
CMAKE_CURRENT_BINARY_DIR resolves __FILE__ relative paths to 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.

Verified locally: ctest -R triangles_unit_tests passes 1.32s from build/ after fix.
Cycle 21 fix for CI run 31058657378 failure.
2026-08-05 17:58:38 -07:00
Krystie d35aec1828 [grade=A] fix(reindex): outer exception handler + per-write TxnAbort + Windows FlushFileBuffers (cycle 20) 2026-08-05 17:06:47 -07:00
Krystie 53a2f0b5d2 [grade=A] fix(chain): disable destructive automatic rebuild (urn:ump:yukhymo2kpp227nesu56snay6mfuctqgglvox476weezc2geij7a) 2026-08-05 14:31:32 -07:00
Krystie 9cb44a2988 [grade=B] fix(i2p): disable i2pd HTTPProxy that crashed daemon on inbound HTTP
The daemon used SetOption("http.enabled", false) which targets the i2pd
WEBCONSOLE (default port 7070). The actual HTTPProxy is configured via
the 'httpproxy.enabled' key (default port 4444). Since the daemon never
set this key, the HTTPProxy ran by default and any inbound HTTP request
on port 4444 crashed the daemon via nullptr dereference in
i2p::i18n::Locale::GetString (m_Language is never initialized).

Live trigger: a simple curl http://127.0.0.1:4444/ brings down the
entire daemon with SIGSEGV. Root cause confirmed via addr2line against
the same offsets on multiple crash events:
  crash_handler
  i2p::i18n::Locale::GetString (m_Language->GetString on nullptr)
  i2p::i18n::translate
  i2p::proxy::HTTPReqHandler::HandleRequest
  i2p::proxy::HTTPReqHandler::HandleSockRecv
  HTTPProxy.cpp:518 (the tr("Host %s is not inside I2P network...") call)

Fix: SetOption("httpproxy.enabled", false) added to InitI2P. The
i2pd.conf also gets a [httpproxy] enabled=false section for
diagnostic consistency (the conf is dead code in the embedded library
path but kept in sync).

Codex grade B (urn:ump: pending final write). Polish suggestions applied:
shortened cycle-13 comments and clarified conf vs runtime SetOption.
2026-08-05 11:42:48 -07:00
Krystie c37102eff4 fix(i2p): port validation phase 0 + try/catch around InitI2P/SetOption/thread (urn:ump:mi2fn54qngckvdwvo6jmyqnobdvmfk36jtjilecwsytmw4hdwy7a grade C, urn:ump:oz7z7vp2tatjs7kzo2ljn5xljvk6zhf6rvzomnq2gsi4vr7k4n4q grade D — partial, polish for v6.2.6.2) 2026-08-05 10:52:30 -07:00
Krystie d0e3657014 [grade=A] chore: bump version to v6.2.6.1 (urn:ump:imqh6eaqup6bgqch3mtuc267myp2ybadxwtpntr2lja3o6ay2rza) 2026-08-05 10:13:04 -07:00
Krystie cb397b6be9 [grade=B urn:ump:w4iifvsmrrra4o25btcpohxwhzvhulh3e22yjeplzw6ms2id33ja] fix(i2p): override SOCKS+SAM ports via SetOption — libi2pd API never reads i2pd.conf 2026-08-05 09:40:42 -07:00
Krystie a1fae5f6f3 i2p: update i2pseed.h with captured live addresses (2026-08-05)
Replace placeholder/stale .b32.i2p addresses in strMainNetI2PSeed with
addresses captured from running daemons via getnetworkinfo on 2026-08-05.

Before: 4 placeholder addresses (SAMI-PC, DNS2, DNS3, Hetzner).
        DNS2/DNS3/Hetzner were NEVER live; the daemon's debug.log
        showed 'lastseen=80-100hrs' for each.

After:  3 live addresses (SAMI-PC, DNS2, DNS3).
        Hetzner ARM64 node has no daemon deployed, so its placeholder
        is removed. Add it back when the tri-pi ARM64 build is deployed
        and produces a live address.

This change makes future daemon startups find I2P peers immediately
via the hardcoded seed list, instead of waiting for the I2P netDb to
populate from initial peers.
2026-08-05 09:08:36 -07:00
Krystie 717f0d07cd [grade=B] rpcnet: allow .b32.i2p addresses in addnode RPC
Triangles is dual-network Tor + I2P, but the addnode RPC rejected any
non-.onion address with HTTP 500 'Only .onion addresses are supported on
this network.'. This prevented runtime addition of I2P peers via the
admin RPC.

Fix: extend the address check to accept both .onion and .b32.i2p
substrings, mirroring the existing pattern. Update the help/error text
to reflect dual-network support.

Codex grade: B (urn:ump:pe5crd53udgv7quryi4n3vqke6y6ndwfj4s2plq4lyabskhybpuq)

Polish deferred: substring matching accepts malformed values
(e.g. 'attacker.onion.invalid'). Existing .onion check has the same
limitation. Future improvement: validate hostname suffixes properly.
2026-08-05 08:51:00 -07:00
9 changed files with 830 additions and 216 deletions
+9
View File
@@ -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
View File
@@ -9,7 +9,7 @@
#define CLIENT_VERSION_MAJOR 6
#define CLIENT_VERSION_MINOR 2
#define CLIENT_VERSION_REVISION 6
#define CLIENT_VERSION_BUILD 0
#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!
+257 -11
View File
@@ -401,6 +401,24 @@ 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;
@@ -409,14 +427,24 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
// 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");
// Write an i2pd.conf configuration file that enables SAM + SOCKS proxy.
// i2pd's config system reads from a file; programmatic option setting is
// fragile across i2pd versions. Writing a minimal conf is robust.
// NOTE: As of i2pd 2.60.0, the embedded library API (i2p::api::InitI2P)
// never calls ParseConfig, so this file is NOT read at runtime. It is
// written for documentation/debugging purposes only — operators can
// inspect it to see what ports the daemon intends to use. The actual
// port bindings are applied programmatically via i2p::config::SetOption
// below (before the background thread starts). Keep the file in sync
// with the SetOption calls.
{
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
std::ofstream conf(confPath.string());
@@ -425,6 +453,8 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
return false;
}
conf << "# Auto-generated by Triangles embedded I2P\n";
conf << "# NOTE: i2pd 2.60.0 library API does NOT read this file.\n";
conf << "# Actual port bindings come from i2p::config::SetOption in i2p_embedded.cpp.\n";
conf << "datadir = " << i2pDataDir << "\n";
conf << "loglevel = info\n";
conf << "\n";
@@ -441,6 +471,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";
@@ -494,14 +531,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);
// ----------------------------------------------------------------
@@ -524,6 +583,64 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
printf("Embedded I2P: launching router in background thread...\n");
fflush(stdout);
// ----------------------------------------------------------------
// PROGRAMMATIC OVERRIDE OF SOCKS/SAM PORTS
//
// i2pd 2.60.0's library API (i2p::api::InitI2P) only calls ParseCmdline
// — it never calls ParseConfig. The i2pd.conf file we just wrote is
// NEVER READ by the embedded library path. SOCKS proxy falls back to
// its built-in default port (4447) regardless of what we put in the
// conf file. This is verified by the library's bundled ParseConfig
// (called only by the standalone daemon binary at
// src/i2p/i2pd-src/daemon/Daemon.cpp:107) — the library API
// deliberately omits it.
//
// The fix: override socksproxy.port + sam.port + socksproxy.address
// AFTER i2p::api::InitI2P returns (so all defaults are in m_Options)
// 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.
// ----------------------------------------------------------------
printf("Embedded I2P: overriding socksproxy.port=%d sam.port=%d via SetOption\n",
socksPort, samPort);
fflush(stdout);
{
bool socksEnabled = true;
std::string socksAddr = "127.0.0.1";
uint16_t socksPortVal = (uint16_t)socksPort;
std::string socksKeys = "socks-proxy.dat";
bool samEnabled = true;
std::string samAddr = "127.0.0.1";
uint16_t samPortVal = (uint16_t)samPort;
// 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;
i2p::config::SetOption("socksproxy.enabled", socksEnabled);
i2p::config::SetOption("socksproxy.address", socksAddr);
i2p::config::SetOption("socksproxy.port", socksPortVal);
i2p::config::SetOption("socksproxy.keys", socksKeys);
i2p::config::SetOption("sam.enabled", samEnabled);
i2p::config::SetOption("sam.address", samAddr);
i2p::config::SetOption("sam.port", samPortVal);
// 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);
}
// Keep the thread handle so Stop() can join it. A detached thread
// that is still running would block the wallet from exiting.
routerThread = std::thread([this]() {
@@ -603,19 +720,138 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
}
}
// Populate .b32.i2p address
// ----------------------------------------------------------------
// Populate .b32.i2p address — use the SERVER TUNNEL destination,
// NOT the embedded router identity.
//
// The Triangles P2P layer listens on the local port via the
// server tunnel loaded from `triangles-p2p-keys.dat`. That tunnel
// publishes a LeaseSet whose destination is the ident hash of
// the keys file (a separate identity from the i2pd router
// itself). Peers that dial the address we advertise must hit
// THAT LeaseSet, or they get SOCKS code 4 / "LeaseSet not found"
// from the floodfill network.
//
// Strategy (preferred first):
// 1. Walk i2p::client::context.GetServerTunnels() and pick the
// server tunnel whose keys file matches
// triangles-p2p-keys.dat — presence in the registry confirms
// the tunnel has registered, so the LeaseSet will be
// published and reachable once the i2pd netDb has it.
// 2. Fall back to parsing triangles-p2p-keys.dat directly via
// i2p::data::PrivateKeys::FromBuffer (binary blob format,
// length == PrivateKeys::GetFullLen()) if the tunnel hasn't
// registered yet (race during the same startup pass).
// 3. Last-resort error log if neither works — better to leave
// i2pHostname empty than advertise the wrong identity.
// ----------------------------------------------------------------
bool advertised = false;
std::string serverKeysPath = (fs::path(i2pDataDir) / "triangles-p2p-keys.dat").string();
// Step 1+2 (combined): authoritative ident hash comes from the
// keys file the server tunnel was loaded from. The tunnel
// registry's GetServerTunnels() maps (IdentHash, port) → tunnel,
// so we just compare each registered tunnel's ident hash
// against what triangles-p2p-keys.dat actually contains. If
// any registered tunnel matches, that's our address. Otherwise
// we fall back to publishing the keys-file ident hash directly
// (the tunnel will register a moment later — the keys file is
// the source of truth either way).
//
// File format: binary blob, length = PrivateKeys::GetFullLen().
// i2pd reads it with FromBuffer() in libi2pd_client/ClientContext.cpp:285-313.
std::string keysFileIdentB32;
try {
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
i2pHostname = identHash.ToBase32() + ".b32.i2p";
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
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 {
printf("Embedded I2P: cannot open %s for server tunnel keys\n",
serverKeysPath.c_str());
}
} catch (const std::exception& e) {
printf("Embedded I2P: keys-file ident hash load failed: %s\n", e.what());
} catch (...) {
printf("Embedded I2P: .b32.i2p address not yet available, Qt timer will retry\n");
printf("Embedded I2P: keys-file ident hash load failed: unknown exception\n");
}
// Try the live registry first — if a registered server tunnel
// matches the keys-file hash, the LeaseSet will be published and
// inbound peers can reach us via that destination.
if (!keysFileIdentB32.empty()) {
try {
for (const auto& kv : i2p::client::context.GetServerTunnels()) {
const i2p::data::IdentHash& dest = kv.first.first;
if (dest.ToBase32() == keysFileIdentB32) {
i2pHostname = keysFileIdentB32 + ".b32.i2p";
advertised = true;
printf("Embedded I2P: server tunnel address (live registry) = %s\n",
i2pHostname.c_str());
break;
}
}
} catch (const std::exception& e) {
printf("Embedded I2P: server tunnel registry read failed: %s\n", e.what());
} catch (...) {
printf("Embedded I2P: server tunnel registry read failed: unknown exception\n");
}
}
// Fall back: trust the keys file even before the tunnel registers.
if (!advertised && !keysFileIdentB32.empty()) {
i2pHostname = keysFileIdentB32 + ".b32.i2p";
advertised = true;
printf("Embedded I2P: server tunnel address (from keys file) = %s\n",
i2pHostname.c_str());
}
// Step 3: explicit failure rather than advertise router identity.
if (!advertised) {
i2pHostname.clear();
printf("Embedded I2P: server tunnel destination not available yet, "
"Qt timer will retry\n");
}
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);
}
});
@@ -627,6 +863,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;
}
+10 -11
View File
@@ -3,24 +3,23 @@
// Hardcoded I2P seed nodes for initial peer discovery.
// These are .b32.i2p addresses (Destination hashes).
// Nodes must run i2pd with a server tunnel forwarding to the Triangles P2P port.
// Nodes must run i2pd (embedded or external) with a server tunnel
// forwarding to the Triangles P2P port.
//
// NOTE: .b32.i2p addresses are derived from the destination's public key.
// They are generated when the node first creates its I2P tunnel keys.
// Replace these placeholders with actual seed node addresses once deployed.
// These addresses were captured from running daemons via getnetworkinfo
// on 2026-08-05. See i2pseed-capture-2026-08-05.md for the raw outputs.
//
// Dynamic seeds will also be available at:
// Dynamic seeds are also available at:
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
static const char *strMainNetI2PSeed[][1] = {
// SAMI-PC - authoritative wallet node (main PC)
// SAMI-PC - authoritative wallet node (main PC). Captured 2026-08-05.
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
// DNS2 - primary bootstrap server (194.233.88.206)
// Generated by embedded i2pd on first run, keys persist in i2p_data/
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
// DNS3 - canonical chain reference (74.208.167.19)
{"hvvr2yys3nll4l6fdywecvn3baw6h5i7bsa2ldbz2e5xwangnn7q.b32.i2p"},
// Hetzner Helsinki - ARM64 staking node (46.62.249.20)
{"2hyeunnkax5du4snip4gdsdicxtmlnagtlkatv57rjpx2kvfssma.b32.i2p"},
// DNS2 - primary bootstrap server (194.233.88.206). Captured 2026-08-05.
{"7d5gujh6tw6xbd2uquedhpm3ixoglsgt3nkfqb4b5lvunhjdb2kq.b32.i2p"},
// DNS3 - canonical chain reference (74.208.167.19). Captured 2026-08-05.
{"jdrpj364rmdule7rw2jdl63wvk3kbaivuje7wyhayugjbxvgbj2a.b32.i2p"},
{nullptr}
};
+116 -101
View File
@@ -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
View File
@@ -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++;
+1
View File
@@ -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);
+4 -4
View File
@@ -143,13 +143,13 @@ Value addnode(const Array& params, bool fHelp)
"addnode <node> <add|remove|onetry>\n"
"Attempts to add or remove a node from the addnode list,\n"
"or try a connection to a node once.\n"
"<node> must be a .onion address (Tor-native network).");
"<node> must be a .onion or .b32.i2p address (Tor+I2P dual-network).");
string strNode = params[0].get_str();
// Tor-native: require .onion addresses
if (strNode.find(".onion") == string::npos)
throw runtime_error("Only .onion addresses are supported on this network.");
// Triangles is dual-network Tor + I2P. Allow both .onion and .b32.i2p addresses.
if (strNode.find(".onion") == string::npos && strNode.find(".b32.i2p") == string::npos)
throw runtime_error("Only .onion or .b32.i2p addresses are supported on this network.");
if (strCommand == "onetry")
{
+42
View File
@@ -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()