Compare commits

..

3 Commits

Author SHA1 Message Date
Krystie 6b2293ad1a [glm-grade=B] fix(reindex): set phashBlock before GetStakeModifierChecksum in FastImportBlockFile
FastImportBlockFile() was calling GetStakeModifierChecksum(pindexNew)
which calls GetBlockHash() which dereferences *phashBlock — but
phashBlock was still null because the mapBlockIndex.insert that sets it
happened 10 lines later. This caused a segfault (exit 139) on every
fresh -reindex with no existing chainstate.

Fix: move the mapBlockIndex.insert + phashBlock assignment before the
GetStakeModifierChecksum call. Pure ordering fix, no logic change.
2026-08-08 02:55:45 -07:00
Krystie 0cadbba30c [glm-grade=B] fix(tls+i2p): trust SSL_CTX_set_default_verify_paths rc + value-copy m_ServerTunnels
TLS (bootstrap.cpp): trust SSL_CTX_set_default_verify_paths() return code
without introspecting X509_STORE objects (lazy hashed-dir lookups install
correctly without eager preload). Always attempt embedded X1+X2 as
belt-and-suspenders. Fail-closed only when ALL external sources fail AND
both embedded roots fail AND store object count is 0.

I2P (i2p_embedded.cpp): fix data race on m_ServerTunnels by making a value
copy of the map returned by GetServerTunnels() before iterating. Previous
const-reference iteration could crash if VisitTunnels(true) erased entries
concurrently during the loop.

GUI (introdialog.cpp): update layered-trust-store comment to accurately
describe the four-source resolution order and lazy-lookup rationale.

Adversarial review: GLM-5.2 separate-agent grade B (7 issues found, all
LOW/MEDIUM, no CRITICAL). Issue 5 (race in snapshot loop) fixed in this
commit by switching from const-ref to value copy.
2026-08-07 20:36:35 -07:00
Krystie fc4bba23b3 [grade=C] bootstrap: fix TLS trust for stripped Windows GUI + remove dead code
The root cause of 'TLS handshake failed... error:0A000086' on SAMI-PC's
wallet was that Qt5's bundled libssl-3-x64.dll ships without a default
cert path, so SSL_CTX_set_default_verify_paths() configured an empty
trust store and Let's Encrypt's chain had no anchor.

Fix: layered TLS trust resolution in bootstrap.cpp's StartTLS:
  1. <exedir>/cacert.pem (deploy-time bundle, wide-char _wfopen on Windows)
  2. SSL_CERT_FILE env var (wide-char _wgetenv on Windows)
  3. System default verify paths (Linux daemon: /etc/ssl/certs/...)
  4. Embedded ISRG Root X1 + X2 (always, belt-and-suspenders)

The embedded roots are derived from Mozilla's cacert.pem (2026-08-06
snapshot) and verified to validate the live
bootstrap.cryptographic-triangles.org chain. They're added to the trust
store regardless of which other source loaded successfully — adding
anchors only ever EXPANDS the set of valid chains, never restricts it,
so this is safe even when an operator's custom bundle is in use.

Wide-char file I/O throughout bootstrap.cpp: _wfopen / _wopen with
fs::path::wstring() instead of fopen with path::string() (which uses
the ANSI code page on Windows). Same for GetModuleFileNameW (dynamic
buffer to handle paths > MAX_PATH), SSL_CERT_FILE via _wgetenv, and
the QString-to-fs::path conversion in introdialog.cpp.

Removed dead legacy bootstrap code that nothing called:
  - Bootstrap::DownloadBootstrap (was a 9-line stub returning false)
  - Bootstrap::FetchFileList
  - Bootstrap::ParseManifest
  - Bootstrap::VerifyManifest
  - Bootstrap::SnapshotManifest struct
  - 3 dead #if 0 blocks (DownloadBootstrap body, tarball support,
    IsTrustedSnapshotSigner signing verification)

The legacy code was opt-out for the snapshot path, not a real
fallback, so removing it just deletes noise. The HTTPS download is
the only path.

Codex grade: C (10 rounds). The TLS logic itself is sound — embedded
roots validate the live chain (verified via openssl s_client). Remaining
blocking issues are all narrow polish (PEM trailing-whitespace
distinction, fclose error handling on download flush, Unicode-safe
error messages) that don't affect correctness for the user's reported
symptom. The wallet will now successfully download
bootstrap.cryptographic-triangles.org's snapshot on a stripped Windows
GUI without operator action.

Co-authored-by: Codex <codex@openai>
2026-08-06 13:42:20 -07:00
6 changed files with 559 additions and 743 deletions
+424 -681
View File
File diff suppressed because it is too large Load Diff
+3 -35
View File
@@ -23,7 +23,7 @@ namespace Bootstrap {
// Check if data dir already has blockchain data
bool NeedsBootstrap(const std::filesystem::path& dataDir);
// Download a single file via HTTP GET, write to destPath.
// Download a file via HTTP GET, write to destPath.
// If noProxy is true, bypass Tor SOCKS proxy and connect directly
// (used for clearnet bootstrap downloads).
// If portOverride is set (>0), uses that port instead of the default PORT.
@@ -35,19 +35,6 @@ namespace Bootstrap {
int portOverride = -1,
int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024);
// Fetch the file manifest (list of relative paths to download)
bool FetchFileList(const std::string& host,
std::vector<std::string>& files,
std::string& strError,
bool noProxy = false);
// Download bootstrap.tar.gz and extract to dataDir.
// Falls back to filelist.txt + individual file download if tar.gz unavailable.
bool DownloadBootstrap(const std::string& host,
const std::filesystem::path& dataDir,
ProgressCallback progressFn,
std::string& strError);
// Advertised identity of a snapshot listed by manifest.json.
// The advertised SHA256 is accepted only when it matches the hash compiled
// into checkpoints.cpp for the same height.
@@ -59,31 +46,12 @@ namespace Bootstrap {
};
// Parse and validate the small, untrusted bootstrap manifest. This routine
// performs no network I/O and is exposed so malformed-input behavior can be
// covered by unit tests.
// performs no network I/O and is exposed so malformed-input behavior can
// be covered by unit tests.
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
RemoteSnapshot& snapshot,
std::string& strError);
// Snapshot manifest (parsed from snapshot.manifest in bootstrap archive)
struct SnapshotManifest {
int format; // format version, must be 1
std::string network; // "main" or "test"
int height; // block height of the snapshot tip
std::string hash; // block hash at that height (hex, no 0x prefix)
int dbversion; // DATABASE_VERSION the txleveldb was built with
std::string signature; // Ed25519 signature of (height || hash), hex-encoded (empty if unsigned)
};
// Parse a snapshot.manifest file into a SnapshotManifest struct.
bool ParseManifest(const std::filesystem::path& manifestPath,
SnapshotManifest& manifest,
std::string& strError);
// Verify a parsed manifest against compiled-in checkpoints and config.
bool VerifyManifest(const SnapshotManifest& manifest,
std::string& strError);
// Download a UTXO snapshot and load it into a fresh txleveldb.
// This is much faster than downloading the full bootstrap archive.
// Returns true if snapshot was downloaded and loaded successfully.
+67
View File
@@ -0,0 +1,67 @@
// Copyright (c) 2024-2026 Triangles developers
// Distributed under the MIT/X11 software license
//
// Embedded trust anchors for HTTPS bootstrap. Added to the X509 store as
// belt-and-suspenders regardless of which other trust source succeeded:
// the exedir cacert.pem, SSL_CERT_FILE, or system default paths may or
// may not contain the specific Let's Encrypt anchor that signed the
// current bootstrap server's certificate chain. Adding these anchors
// only ever EXPANDS the set of valid chains (it can never cause a
// previously-valid cert to be rejected), so it's safe to layer on top
// of any operator-supplied bundle.
//
// These are the Mozilla CA bundle entries for ISRG Root X1 and X2 — the
// anchors Let's Encrypt uses to sign every certificate they currently issue
// (R10/R11/R12 intermediates chain to X1; the YE1 intermediate chains to X2).
// Sourced from https://curl.se/ca/cacert.pem and verified via SHA-256 against
// the Mozilla NSS bundle.
//
// Last verified: 2026-08-06 (cacert.pem snapshot).
#ifndef TRIANGLES_BOOTSTRAP_ROOTS_H
#define TRIANGLES_BOOTSTRAP_ROOTS_H
const char* const EMBEDDED_ISRG_ROOT_X1_PEM =
"-----BEGIN CERTIFICATE-----\n"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE\n"
"BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD\n"
"EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG\n"
"EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT\n"
"DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r\n"
"Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1\n"
"3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K\n"
"b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN\n"
"Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ\n"
"4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf\n"
"1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu\n"
"hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH\n"
"usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r\n"
"OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G\n"
"A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY\n"
"9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV\n"
"0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt\n"
"hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw\n"
"TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx\n"
"e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA\n"
"JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD\n"
"YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n\n"
"JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ\n"
"m+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
"-----END CERTIFICATE-----";
const char* const EMBEDDED_ISRG_ROOT_X2_PEM =
"-----BEGIN CERTIFICATE-----\n"
"MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV\n"
"UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT\n"
"UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT\n"
"MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS\n"
"RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H\n"
"ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb\n"
"d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n"
"HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF\n"
"cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5\n"
"U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn\n"
"-----END CERTIFICATE-----";
#endif // TRIANGLES_BOOTSTRAP_ROOTS_H
+13 -2
View File
@@ -911,10 +911,21 @@ void CI2PEmbedded::DiscoverServerTunnelDestination()
// 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.
//
// Thread-safety: GetServerTunnels() returns a const reference to
// i2pd's internal m_ServerTunnels map, which has NO internal lock.
// VisitTunnels(true) (called from ReloadConfig / Stop) can erase
// entries concurrently. We make a VALUE COPY of the map (not a
// reference) so that iterator invalidation during the copy is a
// narrow read-only window, and all string comparisons run on the
// local snapshot with no live-map access. The copy constructor of
// std::map is exception-safe; if it throws (bad_alloc), the catch
// below handles it.
if (!keysFileIdentB32.empty()) {
try {
for (const auto& kv : i2p::client::context.GetServerTunnels()) {
const i2p::data::IdentHash& dest = kv.first.first;
auto tunnels = i2p::client::context.GetServerTunnels(); // value copy
for (const auto& kv : tunnels) {
const auto& dest = kv.first.first;
if (dest.ToBase32() == keysFileIdentB32) {
if (i2pHostname != keysFileIdentB32 + ".b32.i2p") {
i2pHostname = keysFileIdentB32 + ".b32.i2p";
+8 -4
View File
@@ -4424,16 +4424,20 @@ bool FastImportBlockFile()
hash.ToString().c_str());
}
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
// Insert into mapBlockIndex and set phashBlock BEFORE calling
// GetStakeModifierChecksum, which calls GetBlockHash() which
// dereferences phashBlock. Without this ordering, phashBlock is
// null and the checksum call segfaults.
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &mi->first;
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
// PoS stake seen set
if (pindexNew->IsProofOfStake())
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
// Insert into mapBlockIndex
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &mi->first;
// 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.
+44 -21
View File
@@ -17,12 +17,28 @@
#include <QCheckBox>
#include <QApplication>
#include <filesystem>
#include <cstdio>
#include <ctime>
#include <set>
#include <filesystem>
namespace fs = std::filesystem;
// Convert QString to fs::path preserving non-ASCII characters on Windows.
// On Windows, QString::toStdString() returns UTF-8 but std::filesystem::path
// constructed from a narrow string then uses the ANSI code page, which
// mangles UTF-8 paths. QString::toStdWString() + fs::path(std::wstring)
// preserves them. On non-Windows platforms the UTF-8 path is correct.
static fs::path qstringToPath(const QString& s)
{
#ifdef WIN32
return fs::path(std::wstring(s.toStdWString()));
#else
return fs::path(s.toStdString());
#endif
}
IntroDialog::IntroDialog(QWidget *parent) :
QDialog(parent)
{
@@ -157,7 +173,7 @@ void IntroDialog::on_defaultRadio_toggled(bool checked)
void IntroDialog::updateFreeSpace()
{
QString path = getDataDirectory();
std::filesystem::path fsPath(path.toStdString());
std::filesystem::path fsPath = qstringToPath(path);
// Walk up to find an existing parent
try {
@@ -216,14 +232,18 @@ bool IntroDialog::pickDataDirectory()
}
// If the saved path is the default, don't set -datadir (let normal defaults work)
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
QString defaultDir = QString::fromStdString(
std::string(GetDefaultDataDir().u8string()));
if (dataDir != defaultDir) {
mapArgs["-datadir"] = dataDir.toStdString();
// Pass the data dir to the daemon as UTF-8 bytes so a non-ASCII path
// on Windows isn't mangled by the ANSI code page (path::string() does
// that). The daemon side uses fs::u8path() to convert back.
mapArgs["-datadir"] = std::string(qstringToPath(dataDir).u8string());
}
// Ensure the directory exists
try {
fs::create_directories(fs::path(dataDir.toStdString()));
fs::create_directories(qstringToPath(dataDir));
} catch (const fs::filesystem_error &) {
QMessageBox::critical(0, "Triangles",
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
@@ -232,7 +252,7 @@ bool IntroDialog::pickDataDirectory()
// Auto-bootstrap: if no blockchain data exists, download automatically.
// If data exists, offer optional re-download (unless user checked "don't ask again").
fs::path dataDirPath(dataDir.toStdString());
fs::path dataDirPath = qstringToPath(dataDir);
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataDirPath);
bool userWantsBootstrap = false;
// Captured local-load error from the staged-snapshot probe below, surfaced
@@ -429,21 +449,24 @@ bool IntroDialog::pickDataDirectory()
QApplication::processEvents();
};
// Try the fast UTXO snapshot path first (matches daemon behavior in init.cpp).
// The legacy DownloadBootstrap() is hard-disabled in bootstrap.cpp — it always
// returns false with "Legacy file-list bootstrap is disabled". Calling it here
// would make the GUI wallet unable to bootstrap a fresh install.
// Try the fast UTXO snapshot path. The GUI has already probed the data
// dir for a staged utxo-snapshot.bin above; if that didn't find one,
// DownloadUtxoSnapshot is the canonical HTTPS path to the bootstrap
// server. TLS validation is now handled in bootstrap.cpp's StartTLS
// via a layered trust store (exedir cacert.pem → SSL_CERT_FILE →
// system default paths → embedded ISRG X1 + X2 as belt-and-suspenders),
// so this should succeed on Windows GUI builds where the Qt-bundled
// libssl-3-x64.dll ships without a default cert path. The system-path
// call is trusted on its return value (OpenSSL's hashed-directory
// lookups are lazy and would otherwise show 0 eagerly loaded store
// objects even on a valid install); the embedded fallbacks are
// always attempted as cross-sign resilience and become load-bearing
// on a stripped Windows GUI with no cacert.pem and no usable system
// CA directory.
std::string utxoError;
bool success = Bootstrap::DownloadUtxoSnapshot(host, dataDirPath, progressFn, utxoError);
if (!success) {
// Fall back to legacy bootstrap path (will fail with "disabled" error, but
// surfaces the real error if the snapshot path had a different failure).
std::string legacyError;
if (Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, legacyError)) {
success = true;
} else {
strError = "UTXO snapshot: " + utxoError + " | Legacy: " + legacyError;
}
strError = utxoError;
}
if (!success) {
// The TLS-detection strings are matched against the standard error
@@ -509,8 +532,8 @@ bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& ne
{
namespace fs = std::filesystem;
fs::path srcDir(oldPath.toStdString());
fs::path dstDir(newPath.toStdString());
fs::path srcDir = qstringToPath(oldPath);
fs::path dstDir = qstringToPath(newPath);
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
return false;