diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c93487e..5a65aaf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -49,6 +49,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive - name: Install dependencies + clang-tidy run: | diff --git a/src/checkpoints.cpp b/src/checkpoints.cpp index ce4e744..5358ef4 100644 --- a/src/checkpoints.cpp +++ b/src/checkpoints.cpp @@ -33,9 +33,10 @@ namespace Checkpoints { 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")}, { 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")}, // Recent finality pin (PoS era). Closes the long unchecked span from - // 17650 to the live tip so stale-bootstrap / low-trust forks below this - // height are rejected outright. Hash taken from the canonical chain. + // 17650 to the live tip so stale-bootstrap / low-trust forks below + // this height are rejected outright. Hash from the canonical chain. { 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")}, + { 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")}, }; // Published UTXO snapshot file SHA256, keyed by snapshot height. @@ -47,6 +48,7 @@ namespace Checkpoints // here. The corresponding (height, blockhash) must already exist in // mapCheckpoints / mapCheckpointsTestnet. static std::map mapSnapshotHashes = { + { 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")}, }; static std::map mapSnapshotHashesTestnet = { diff --git a/src/clientversion.h b/src/clientversion.h index d6abcaf..0416943 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -8,7 +8,7 @@ // These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 5 #define CLIENT_VERSION_MINOR 9 -#define CLIENT_VERSION_REVISION 12 +#define CLIENT_VERSION_REVISION 15 #define CLIENT_VERSION_BUILD 0 // Converts the parameter X to a string after macro replacement on X has been performed. diff --git a/src/init.cpp b/src/init.cpp index 51a1290..4ea96f1 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1089,6 +1089,37 @@ bool AppInit2() if (!LoadBlockIndex()) return InitError(_("Error loading blkindex.dat")); + // triangles fix (pitfall #61): initialize pindexFinalized from the + // hardcoded checkpoint on startup, BEFORE the daemon opens any peer + // connections or processes any block messages. + // + // Without this, pindexFinalized stays NULL on a fresh restart even when + // we have 2.2M blocks on disk, because the auto-checkpoint code in + // ActivateBestChain() at main.cpp:2459 only sets it when + // !IsInitialBlockDownload(). If the chain tip is more than 24h stale + // (which happens on every restart with a synced chain), IsInitialBlockDownload() + // returns true and pindexFinalized never gets set. + // + // The downstream reorg guard at main.cpp:2198 short-circuits when + // pindexFinalized is NULL, which allowed a 3,755-block minority fork + // to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading + // the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on + // startup means the reorg guard is always active whenever the + // checkpointed block is in our local mapBlockIndex. + { + CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex); + if (pCheckpoint && pCheckpoint != pindexFinalized) + { + pindexFinalized = pCheckpoint; + printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n", + pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str()); + } + else if (!pCheckpoint) + { + printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n"); + } + } + // If the block index is empty but blk0001.dat exists (bootstrap download), // fast-import: build the index directly from the block file without re-writing // data. Batches LevelDB commits every 200K blocks for speed. diff --git a/src/main.cpp b/src/main.cpp index f109d39..da755ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4500,8 +4500,60 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { // Find the last block the caller has in the main chain pindex = locator.GetBlockIndex(); + + // triangles fix: handle broken pnext chain. + // GetBlockIndex() returns pindexGenesisBlock when no locator + // hash matches our main chain (peer is on a different fork or + // a stale local state). pindexGenesisBlock->pnext is always + // null, which would cause the for-loop below to send ZERO + // headers, leaving the peer stuck (logged as "getheaders -1"). + // + // Mirror the getblocks handler: if the locator matches nothing + // on our main chain, serve our headers from genesis so the peer + // can discover the canonical chain. Then fall back to a tip- + // backwards walk if pnext is null for any other reason (this + // happens when LoadBlockIndex() didn't fully heal pnext links, + // or the chain was bootstrapped from a snapshot). + // + // pitfall #61 guard: if pindexFinalized is set (from the startup + // hardcoded-checkpoint init in init.cpp), serve from there instead + // of genesis. This prevents a fork peer from feeding us their + // short chain back via getheaders — the peer only learns our + // canonical chain from the finalized point forward, and their + // conflicting fork gets rejected at the reorg check in + // Reorganize() because the fork point is below pindexFinalized. + if (!locator.IsNull() && pindex == pindexGenesisBlock && + pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash()) + { + if (pindexFinalized && pindexFinalized->pnext) + { + printf("getheaders: fork detected from peer %s, serving headers from finalized block %d (not genesis) — pitfall #61 guard\n", + pfrom->addr.ToString().c_str(), pindexFinalized->nHeight); + pindex = pindexFinalized; + } + else + { + printf("WARNING: peer getheaders locator has no common blocks — serving headers from genesis (peer may be on a fork)\n"); + pindex = pindexGenesisBlock; + } + } + if (pindex) - pindex = pindex->pnext; + { + if (pindex->pnext) + { + pindex = pindex->pnext; + } + else + { + // pnext is null — fall back to walking from pindexBest + // backwards to find the block immediately after pindex + CBlockIndex* pWalk = pindexBest; + while (pWalk && pWalk->pprev != pindex) + pWalk = pWalk->pprev; + pindex = pWalk; // null if pindex is already the tip + } + } } vector vHeaders; diff --git a/src/tor/tor_embedded.cpp b/src/tor/tor_embedded.cpp index 8dfd317..9b1f4d2 100644 --- a/src/tor/tor_embedded.cpp +++ b/src/tor/tor_embedded.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -122,11 +124,59 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService) // Prepare Tor data directory under the wallet's data dir torDataDir = (::GetDataDir() / "tor_data").string(); fs::create_directories(torDataDir); + // CRITICAL: Tor refuses to use a DataDirectory readable by other users. + // Without 0700, tor_run_main() returns -1 and the embedded Tor never starts. + fs::permissions(torDataDir, fs::perms::owner_all, fs::perm_options::replace); + + // triangles fix: auto-repair `state`-as-file corruption (pitfall #19). + // Tor's atomic state-write pattern is: write `state.tmp` → rename to `state`. + // If the daemon is killed or the process crashes mid-write, the rename can + // fail and `state` may be left as a regular file (or a partial file). On + // next start, Tor sees "State file ... is not a file? Failing." and dies + // with code -1 ("Reading config failed"). This was hit on DNS3 on + // 2026-05-24 and on the TRI-LAPTOP GUI wallet on 2026-06-15. The user-facing + // symptom is "Tor failed to start. Triangles requires Tor to operate." and + // the only fix was manually renaming the corrupt file. Detect this state + // here and auto-rename so the daemon is self-healing. + { + fs::path statePath = fs::path(torDataDir) / "state"; + std::error_code ec; + if (fs::exists(statePath, ec) && !fs::is_directory(statePath, ec)) { + // state is a file (or symlink to one) — quarantine it + auto now = std::chrono::system_clock::now(); + auto t = std::chrono::system_clock::to_time_t(now); + char ts[32]; + std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", std::gmtime(&t)); + fs::path quarantine = fs::path(torDataDir) / + (std::string("state.corrupt-") + ts); + try { + fs::rename(statePath, quarantine, ec); + if (ec) { + // rename can fail on Windows if dest exists; remove then rename + fs::remove(quarantine, ec); + fs::rename(statePath, quarantine, ec); + } + printf("Tor state was a file (corrupt) — quarantined to %s for inspection. Tor will recreate state/ as a directory.\n", + quarantine.filename().string().c_str()); + } catch (const std::exception& e) { + printf("WARNING: could not quarantine corrupt Tor state file %s: %s\n", + statePath.string().c_str(), e.what()); + // Last resort: try to remove it so Tor can proceed + fs::remove(statePath, ec); + } + } + } std::string hsDir; if (hiddenServiceEnabled) { hsDir = (fs::path(torDataDir) / "hidden_service").string(); fs::create_directories(hsDir); + // CRITICAL: Tor rejects hidden service directories that are not 0700 + // ("Permissions on directory ... are too permissive") and aborts config + // validation with code -1. This was the root cause of "Embedded Tor + // exited with code -1" — fs::create_directories honors umask (0022 on + // most Linux systems), leaving the dir at 0755. Force 0700 after creation. + fs::permissions(hsDir, fs::perms::owner_all, fs::perm_options::replace); } // Build the argv for tor_run_main