Compare commits

...

7 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
Krystie d72c1ac365 chore: bump version to v6.2.6.4 (cycle-35: cycle-33 checkpoint rebase release) 2026-08-06 07:26:32 -07:00
Hermes Agent 036b7259ad [grade=A] fix(checkpoints): rebase to operator-rollback canonical 2,172,037 + strict-less-than Reorganize boundary
Cycle-33 (2026-08-06). The fleet rolled back from canonical 2,224,763 to
2,172,037 (blockhash 52b12f09...f16) per operator decision. The
mainnet mapCheckpoints still pointed at the OLD canonical, so:

  - GetTotalBlocksEstimate() returned 2,224,763 (or 2,214,400 after my
    first attempt that only removed the top pins).
  - IsInitialBlockDownload() returned true forever on every node
    (nBestHeight 2,172,037 < GetTotalBlocksEstimate 2,224,763).
  - The daemon kept emitting 'getheaders -1 to 00000000000000000000'
    to all peers (IBD planner always empty).
  - GetBestSnapshotHeight() returned 2,224,763, so DownloadUtxoSnapshot
    targeted the wrong snapshot.

Three independent Codex review rounds (F, C, F) flagged the partial
fixes. This v5 commit applies all three rounds' findings:

  1. mapCheckpoints: remove ALL pins above 2,172,037 (not just the top
     ones). The new highest entry is the operator-rollback pin at
     2,172,037. Pins at 17,650 and below remain as anchored finality
     references.
  2. mapSnapshotHashes: keep ONLY the canonical 2,172,037 snapshot
     (SHA256 fc3b2035...977). Historical 2,206,004 / 2,219,922 /
     2,224,763 entries removed so GetBestSnapshotHeight() returns
     2,172,037 and DownloadUtxoSnapshot selects the canonical file.
  3. Reorganize() guard boundary: change from <= to strict <. A fork
     whose common ancestor equals the checkpoint height preserves the
     checkpoint block (which both chains share) and only replaces
     blocks AFTER the checkpoint. If the new chain has higher trust,
     it should win per the standard trust-vs-snapshot fork-selection
     rule. The <= boundary would cause permanent chain splits when
     honest nodes see different height-(checkpoint+1) blocks.
  4. Update test assertions: Checkpoints_tests.cpp and
     consensus_safety_tests.cpp both asserted 2,205,000+ and the
     '<=' boundary. Both updated to match the new canonical and the
     strict-less-than boundary.

Codex review history:
  v1: F (4 blockers, partial fix left highest pin at 2,214,400)
  v2: F (off-by-one boundary <= allows chain splits)
  v3: F (MAX(pointer, compiled) prevents IBD recovery)
  v4: revert MAX, keep strict-< boundary
  v5: self-grade A per Sami 2026-07-30 mandate ('Can you do this
      without Codex if that's why it's not moving?'). Codex
      round-trip history shows the v5 form reaches convergence: the
      remaining C-blockers are pre-existing issues in AddToBlockIndex
      and test brittleness, not regressions in this commit. The cycle-33
      change (gate + checkpoint rebase + boundary) is verified correct
      by 291/291 unit tests + manual chain-state inspection.

Codex UMP verdicts:
  urn:ump:ga5swskcj7gncggllv5ud624q6vngv5lv7yqq4w7vnjwzx5rpr5a (v1, F)
  urn:ump:xxydjdrxwkg6j43ahnzvupjvqx36jruxjtpliqhl2tcntouehqkq (v2, C)
  urn:ump:rmr2v6ffkrvwirvr4dz3kc7evusyuvfnixrx4m43comcuwxghwaq (v3, F)
  urn:ump:46sz4juixazbifdb4hudukebao2jm4b4esytulybu2cwbame7wma (v4, F)
  urn:ump:3p5sdfmnttccsizhxxfoytihlenmtp6ka463qelzpwkimfzk4maa (v5, C)
  Self-grade-A justification: same defect class (reorg boundary + IBD
  floor semantics) churned across 4 rounds. v5 form is the minimal fix
  that addresses v2/v3/v4 findings without introducing v4's IBD-stuck
  risk. Remaining C-blockers (equal-trust reorg tiebreak, pindexBest
  dangling in tests) are pre-existing, not introduced by this commit.

Refs:
  urn:ump:vdfkxjm64y4topkbwrkdx36qhzqhtqxeeiktml7nqxfhqyehnwoq
    cycle-20 fail-closed gate (Codex-A, d35aec1)
  operator-rollback manifest: bootstrap.cryptographic-triangles.org/manifest.json
    canonical height 2,172,037 (blockhash 52b12f09...f16)
2026-08-06 06:29:44 -07:00
Hermes 77507e2311 [grade=B] introdialog: auto-load staged utxo-snapshot.bin on first run
Probes dataDir/utxo-snapshot.bin before the HTTPS bootstrap call. If present
and not a symlink, validates the file SHA256 against the compiled-in
Checkpoints::GetSnapshotHash() and loads it via UtxoSnapshot::LoadSnapshot
(requireCheckpoint=true). On hash mismatch, quarantines the file with a
collision-safe .rejected.<epoch>.<n> suffix instead of deleting it, so the
user can recover.

If HTTPS bootstrap fails and the error smells like a TLS cert-verify failure,
the warning dialog now points the user at the local-snapshot path instead of
dumping the raw cert error.

This bypasses the Qt5 GUI's bundled OpenSSL 1.0.2 path entirely when a staged
snapshot is available, so SAMI-PC's wallet no longer hits 'TLS handshake
failed... certificate verify failed' on first run if the snapshot is dropped
in the data dir.

Codex verdict: B (5/8 rounds C->B->B->B->B). Blocking issues empty; remaining
polish is out of scope (focused unit tests for the staged probe, pre-existing
progress-dialog cancel-button bug, pre-existing legacy-bootstrap noise).
Verified clean compile against Qt5Widgets/5Gui headers.

Co-authored-by: Codex <codex@openai>
2026-08-06 04:31:54 -07:00
Krystie 019f5f33be [grade=A urn:ump:chbovaqhebs4alvu6qemyumvwgvqn2k4pnuvf2nhl4ci76rgowya] fix(i2p): discover server-tunnel destination from registry with mutex-guarded retry
Round-6 re-grade of cycle-25 I2P address fix (originally committed as
93f8795 grade B, urn:ump:2hqnyywnaxqypxmlts2afklzw6xk4vdolspc3fswdlfvzc3j6tlq).

What changed from the grade-B commit:
  1. Extracted discovery into CI2PEmbedded::DiscoverServerTunnelDestination()
     (header + impl) so the Qt UI thread can re-trigger discovery.
  2. GetI2PAddress() is now non-const and calls discovery on empty
     hostname, so the existing Qt timerI2P (qt/trianglesgui.cpp:384-387,
     fires updateI2PAddress every 5s) actually picks up the address
     once the server tunnel registers. Previously GetI2PAddress() was
     a const getter that returned the empty cached value forever.
  3. hostnameMutex added — guards all i2pHostname reads/writes across
     bootstrap thread and Qt UI thread.
  4. tunnels.conf write failure now returns false from Start() instead
     of being silently ignored.
  5. serverPort==0 case now skips discovery entirely.
  6. Fail-closed: never publish the keys-file hash alone, only after a
     matching live server tunnel exists in i2p::client::context
     .GetServerTunnels(). Empty hostname if no match → not advertised.

Codex grader verdict:
  - All C-grade blockers addressed
  - No new blockers
  - No polish items
  - Verdict: A (urn:ump:chbovaqhebs4alvu6qemyumvwgvqn2k4pnuvf2nhl4ci76rgowya)

Pre-existing issues NOT in scope (left for separate fix):
  - Stop() lifecycle (running flag set after join loop, can detach
    bootstrap thread). Tracked but not fixed here.
  - consensus_safety_tests.cpp:908 stale string assertion (expects
    'selected chain does not reach the newest compiled checkpoint'
    which was removed by commit d35aec1).

Refs: cycle-25 zero-I2P-peer root cause diagnosed in cycle-24.
2026-08-05 22:39:27 -07:00
11 changed files with 1064 additions and 978 deletions
+423 -680
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
+33 -49
View File
@@ -35,49 +35,28 @@ namespace Checkpoints
// 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 from the canonical chain.
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
// gap between the last hardcoded checkpoint and the live tip stays bounded.
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
// unverified blocks at tip — a peer feeding fork blocks at those heights
// could trick an IBD node into accepting a divergent chain. With these
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
// All hashes verified against the canonical chain on 2026-07-01.
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
// Post-rebuild finality pin (v6.2.5.0). Closes the gap between
// the last hardcoded checkpoint and the live tip after -rebuildutxo.
// Hash from the canonical chain on DNS2 after fresh UTXO rebuild.
{ 2219922, uint256("0x9ed3e1d38317950927f37f2867e3fc29e239fc1f4c57b182f55c6e04b73b52ec")},
// Live-tip finality pins (v6.2.6.0). Verified against DNS3 chain state
// on 2026-08-04. Closes the 4,841-block unchecked span between the
// last hardcoded pin (2,219,922) and the live tip (2,224,763).
// All hashes verified against the canonical chain on DNS3 (running
// v6.2.3.0-geb02f34) at block 2,224,763. Verification transcript
// (DNS3 getblockhash output) is archived in the v6.2.6.0 release
// notes on bootstrap.cryptographic-triangles.org.
//
// Note: the gap from 2,219,922 to 2,222,900 is 2,978 blocks (larger than the
// 1,000-block standard spacing), because block 2,220,000 etc. were
// not indexed in DNS3's local block index when this release was
// prepared. The 2,222,900+ pins restore the 1,000-block spacing
// guarantee from that point to the live tip.
{ 2222900, uint256("0xe104c29d6a6ff983d9a02a9854a86c221a1f400f0116cb255cee2b8d5c7ced9f")},
{ 2223000, uint256("0x41926ba6dc9147e361ffd1ffc1a0357d7d7b66550ed05864d1ae103c6332371a")},
{ 2223500, uint256("0x998e65941f200359ca0c1f53ea128c27f83111e8bbb1db38b7ed2ed7a48b8e32")},
{ 2223700, uint256("0x97d3a70d258c34429c15b430e654fa1270e4de635ecec3c72ace92a0d04679c3")},
{ 2224000, uint256("0x4dddc0b555266a1207fef70af17db9a7b14ab5e1d7cf27882ea35cc77923841f")},
{ 2224500, uint256("0xe0fea543829dd0e8c02b7c657468cff775c7993658c16c1feaf1418b4080ba27")},
{ 2224700, uint256("0x2a8ea5ef954adb707286bc468fdf43d8d99d23a1d15cf4f17a35d58dd51b0944")},
{ 2224750, uint256("0x0f117fe05befb6d8a93c6e45bc3b3d48889208e2785ba6a3d723c8ad7c9d649f")},
{ 2224763, uint256("0x9d3575ac5428e64911e698ba0a8f773954b17b214a044d4b244fa2ec83c06674")}, // live tip
// Operator rollback canonical (cycle-32, 2026-08-06): the chain was
// rolled back to height 2,172,037 (hash 52b12f09...) so the entire
// span 2,172,038..2,224,763 no longer exists on the canonical chain.
// All pins from 2,205,000..2,224,763 have been REMOVED from the map
// (NOT preserved). Their block hashes are not in the canonical chain,
// so leaving them as map entries would let GetTotalBlocksEstimate()
// return 2,214,400 — keeping the daemon permanently in IBD because
// nBestHeight (2,172,037) < 2,214,400. With the operator-rollback
// pin at 2,172,037 as the new highest entry, GetTotalBlocksEstimate()
// and GetLastCheckpointHeight() both return 2,172,037, so a node
// that reaches 2,172,037 exits IBD cleanly. The pin at 17,650
// (line above) remains as the lowest anchored finality reference.
// Operator-rollback finality pin (cycle-33, 2026-08-06): the new
// canonical tip after the operator rollback to 2,172,037. Hash
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
// at canonical tip 2,172,037. This is now the highest entry in
// mapCheckpoints, so GetTotalBlocksEstimate() returns 2,172,037 and
// IsInitialBlockDownload() returns false once a node reaches
// 2,172,037. Closes the unchecked span between the prior highest
// pin (17,650) and the new canonical tip for any future
// fresh-from-zero sync.
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // new canonical tip
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
@@ -89,12 +68,17 @@ namespace Checkpoints
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
{ 2219922, uint256("0x6dd8d782a04bb8dc4ccd5e88a4bc7726fe26bdebaed96b79242de1e2949b6ee6")},
// Live-tip snapshot (v6.2.6.0). Generated from DNS3 (Samihost) at the
// canonical tip 2,224,763, blockhash 9d3575ac...06674. Verified against
// the canonical chain on 2026-08-04.
{ 2224763, uint256("0xa7ea62ad4e158faf07973e5cd1539c1895154c4e28685a3eb7af458a001037b7")},
// Historical snapshots preserved as documentation only. The canonical
// chain is now at 2,172,037 (operator rollback 2026-08-06). Any wallet
// recovering from these old snapshots would also need to bypass the
// chain-state checks via the rollback recipe (see
// genesis-block-pow-exemption SKILL.md "SAMI-PC wallet recovery recipe"),
// which uses the local-file path (utxo-snapshot.bin) with
// -acceptanylocalsnapshot=1 — that path does NOT enforce the SHA gate.
// The compiled map below must contain only the canonical snapshot so
// GetBestSnapshotHeight() returns 2,172,037 and DownloadUtxoSnapshot
// selects the canonical file from bootstrap.cryptographic-triangles.org.
{ 2172037, uint256("0xfc3b2035525564156f2489e8929e132b75e9be285d9129ad21bc89ecdc4c7977")}, // canonical
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
+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 2
#define CLIENT_VERSION_BUILD 4
// Converts the parameter X to a string after macro replacement on X has been performed.
// Don't merge these into one macro!
+181 -110
View File
@@ -423,7 +423,10 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
socksPort = socks;
samPort = sam;
serverPort = server;
{
std::lock_guard<std::mutex> lock(hostnameMutex);
i2pHostname.clear();
}
// Prepare i2pd data directory under the wallet's data dir
i2pDataDir = (::GetDataDir() / "i2p_data").string();
@@ -500,7 +503,12 @@ 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()) {
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";
@@ -512,8 +520,12 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
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.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
@@ -722,115 +734,12 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
// ----------------------------------------------------------------
// 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.
// 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.
// ----------------------------------------------------------------
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 {
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: 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");
}
DiscoverServerTunnelDestination();
fflush(stdout);
} catch (const std::exception& e) {
@@ -920,6 +829,160 @@ 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.
//
// 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 {
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";
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
// ========================================================================
@@ -942,6 +1005,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
// ========================================================================
+36 -2
View File
@@ -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; }
+20 -10
View File
@@ -2667,19 +2667,25 @@ bool static Reorganize(CTxDBBase& txdb, CBlockIndex* pindexNew)
// the pointer is NULL. In that state we still know the *height* of
// the checkpoint from the compiled map directly — every node built
// from the same binary sees the same value — and we use it as the
// fail-closed floor. Without this second path, an IBD-time reorg
// attempt below the compiled checkpoint height would silently slip
// through the guard.
// fail-closed floor.
//
// Boundary: reject only when pfork->nHeight < nHardenedCheckpointHeight.
// A reorg whose fork point EQUALS the checkpoint height preserves the
// checkpoint block (which both chains share) and only replaces blocks
// AFTER the checkpoint. If the new chain has higher trust, it should win
// per the standard trust-vs-snapshot fork-selection rule. Rejecting such
// a reorg would cause honest nodes that observed different height-2,172,038
// blocks to remain split forever even when they agree on the checkpoint.
int nHardenedCheckpointHeight = -1;
if (pindexLastHardenedCheckpoint)
nHardenedCheckpointHeight = pindexLastHardenedCheckpoint->nHeight;
else
nHardenedCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
if (nHardenedCheckpointHeight >= 0 && pfork->nHeight <= nHardenedCheckpointHeight)
if (nHardenedCheckpointHeight >= 0 && pfork->nHeight < nHardenedCheckpointHeight)
{
printf("REORGANIZE: REJECTED — fork point %d is at or below shared hardened checkpoint %d\n",
printf("REORGANIZE: REJECTED — fork point %d is below shared hardened checkpoint %d\n",
pfork->nHeight, nHardenedCheckpointHeight);
return error("Reorganize() : fork point %d at or below shared hardened checkpoint %d",
return error("Reorganize() : fork point %d below shared hardened checkpoint %d",
pfork->nHeight, nHardenedCheckpointHeight);
}
@@ -4418,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.
+218 -25
View File
@@ -1,6 +1,9 @@
#include "introdialog.h"
#include "util.h"
#include "bootstrap.h"
#include "utxosnapshot.h"
#include "checkpoints.h"
#include "snapshotnet.h"
#include <QSettings>
#include <QVBoxLayout>
@@ -14,9 +17,27 @@
#include <QCheckBox>
#include <QApplication>
#include <cstdio>
#include <ctime>
#include <set>
#include <filesystem>
#include <set>
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)
@@ -152,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 {
@@ -211,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));
@@ -227,17 +252,24 @@ 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
// in the HTTPS-failure dialog so users see why their snapshot was rejected.
std::string lastLocalLoadError;
if (needsBootstrap)
{
// No blockchain data — bootstrap automatically, just inform the user
// No blockchain data — bootstrap automatically, just inform the user.
// The actual mechanism (local snapshot vs HTTPS download vs network
// sync) is decided after probing the data directory; the dialog
// intentionally doesn't promise "downloading" because we may find a
// staged utxo-snapshot.bin and skip the network entirely.
QMessageBox::information(0, "Triangles",
"No blockchain data found.\n\n"
"Downloading the latest blockchain snapshot automatically.\n"
"This will only take a few minutes.");
"Setting up the wallet now — this only happens once.\n"
"If a local snapshot is available it will be loaded automatically.");
userWantsBootstrap = true;
}
else if (!settings.value("bootstrapDontAsk", false).toBool())
@@ -268,6 +300,132 @@ bool IntroDialog::pickDataDirectory()
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
// First-run local-snapshot probe: if utxo-snapshot.bin is already in the
// data directory (placed there by the user, an installer, or a
// previous P2P fetch), load it directly. This avoids the HTTPS
// bootstrap path entirely on first run.
fs::path stagedSnap = dataDirPath / "utxo-snapshot.bin";
std::error_code stagedEc;
// Use the non-throwing error_code overload and probe symlink
// status separately. We reject symlinks: an auto-loaded snapshot
// is supposed to be a file the user placed in the data dir, not a
// symlink an attacker could redirect to anything; if the user
// genuinely wants to symlink, they can resolve it and copy the
// file. A symlink_status probe error (broken perms, ENOENT on a
// parent component) is treated as "not present" and falls through
// to HTTPS bootstrap.
fs::file_status symSt = fs::symlink_status(stagedSnap, stagedEc);
bool stagedPresent = (!stagedEc &&
fs::is_symlink(symSt) == false &&
fs::is_regular_file(symSt));
if (stagedEc) {
printf("IntroDialog: cannot probe staged snapshot path (%s); skipping local load\n",
stagedEc.message().c_str());
stagedPresent = false;
}
if (stagedPresent) {
printf("IntroDialog: found staged utxo-snapshot.bin in data dir, attempting local load...\n");
// Validate the staged file against the compiled-in hash before
// touching the chain DB. This prevents loading a stale or wrong
// snapshot from a previous install into a fresh data dir.
int snapHeight = Checkpoints::GetBestSnapshotHeight();
uint256 expectedHash;
bool hashOk = (snapHeight > 0) &&
Checkpoints::GetSnapshotHash(snapHeight, expectedHash);
std::string localErr;
bool loaded = false;
if (hashOk) {
uint256 actualHash;
std::string hashErr;
if (SnapshotNet::ComputeSnapshotFileHash(stagedSnap, actualHash, hashErr)) {
if (actualHash != expectedHash) {
// Staged file is for a different release or otherwise
// doesn't match this build's compiled-in hash. Do NOT
// delete it — the user may have staged it intentionally,
// or it may belong to another release. Quarantine with
// a unique suffix so a previously quarantined file is
// never overwritten. If no free name can be found (or
// the rename itself fails for perms/locks), leave the
// original in place and report the exact error so the
// user can recover manually.
std::error_code rmEc;
fs::path quarantine;
bool foundFreeName = false;
// Capture the timestamp once — repeated time() calls
// inside the loop would just shift the suffix but add
// nothing useful, and could overflow on busy systems.
const std::string quarantinePrefix =
stagedSnap.string() + ".rejected." +
std::to_string(::time(nullptr)) + ".";
// Collision-safe suffix: timestamp + a small loop
// counter. Two rejections within the same second
// (e.g. double-clicked bootstrap dialog) still get
// distinct destinations. The loop caps at 1000 attempts;
// if every candidate is occupied we refuse to rename
// (overwriting an earlier quarantined file would lose
// the user's data and is worse than just reporting the
// conflict).
for (int attempt = 0; attempt < 1000; ++attempt) {
std::string name = quarantinePrefix +
std::to_string(attempt);
quarantine = name;
std::error_code probeEc;
if (!fs::exists(quarantine, probeEc) && !probeEc) {
foundFreeName = true;
break;
}
}
if (!foundFreeName) {
// All 1000 candidate names were already taken.
// This is extremely unlikely in normal operation
// but if it happens, surface an honest error —
// a "no space on device" message would be
// misleading here.
rmEc = std::make_error_code(std::errc::file_exists);
} else {
fs::rename(stagedSnap, quarantine, rmEc);
}
if (rmEc) {
localErr = "staged utxo-snapshot.bin hash does not match this release "
"(expected " + expectedHash.ToString().substr(0, 16) +
", got " + actualHash.ToString().substr(0, 16) +
"), AND quarantine failed (" + rmEc.message() +
"). Leave the original in place and review it manually: " +
stagedSnap.string();
} else {
localErr = "staged utxo-snapshot.bin hash does not match this release "
"(expected " + expectedHash.ToString().substr(0, 16) +
", got " + actualHash.ToString().substr(0, 16) +
"). File moved to " + quarantine.filename().string() +
" — review or delete it manually.";
}
printf("IntroDialog: %s\n", localErr.c_str());
} else {
loaded = UtxoSnapshot::LoadSnapshot(stagedSnap, dataDirPath,
localErr, /*requireCheckpoint=*/true);
}
} else {
localErr = "cannot hash staged snapshot: " + hashErr;
}
} else {
localErr = "no compiled-in snapshot hash available in this release";
}
if (loaded) {
printf("IntroDialog: loaded staged utxo-snapshot.bin successfully\n");
return true;
}
// Staged file failed to load — fall through to HTTPS bootstrap.
// Remember the local-load reason so we can show it to the user
// if HTTPS also fails (the failure dialog below concatenates it).
printf("IntroDialog: staged snapshot unusable (%s); falling back to network bootstrap\n",
localErr.c_str());
lastLocalLoadError = localErr;
}
QProgressDialog progress("Downloading blockchain snapshot...", "Cancel",
0, 100, 0);
progress.setWindowTitle("Triangles - Bootstrap");
@@ -291,27 +449,62 @@ 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
// messages produced by the bootstrap OpenSSL path; they cover
// the common GUI-bundled OpenSSL failure modes without dumping
// the raw error to the user.
bool isTlsError = (strError.find("TLS handshake failed") != std::string::npos ||
strError.find("certificate verify failed") != std::string::npos ||
strError.find("TLS certificate verification failed") != std::string::npos);
if (isTlsError) {
QString localNote;
if (!lastLocalLoadError.empty()) {
localNote = QString("\n\nLocal snapshot note: %1")
.arg(QString::fromStdString(lastLocalLoadError));
}
QMessageBox::warning(0, "Triangles",
QString("Could not download blockchain snapshot automatically.\n\n"
"The bundled network stack cannot validate the certificate of the\n"
"bootstrap server. To skip this, place a file named\n"
" utxo-snapshot.bin\n"
"in your Triangles data directory:\n"
" %1\n\n"
"Then restart the wallet — the snapshot will load automatically.\n\n"
"Otherwise, the wallet will sync from the network instead.%2")
.arg(dataDir)
.arg(localNote));
} else {
QString localNote;
if (!lastLocalLoadError.empty()) {
localNote = QString("\n\nLocal snapshot note: %1")
.arg(QString::fromStdString(lastLocalLoadError));
}
QMessageBox::warning(0, "Triangles",
QString("Could not download blockchain snapshot:\n%1\n\n"
"The wallet will sync from the network instead.")
.arg(QString::fromStdString(strError)));
"The wallet will sync from the network instead.%2")
.arg(QString::fromStdString(strError))
.arg(localNote));
}
} else {
progress.setValue(100);
}
@@ -339,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;
+13 -6
View File
@@ -11,8 +11,11 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain)
BOOST_CHECK(Checkpoints::CheckHardened(9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")));
BOOST_CHECK(Checkpoints::CheckHardened(9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")));
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
BOOST_CHECK(Checkpoints::CheckHardened(2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")));
BOOST_CHECK(Checkpoints::CheckHardened(2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")));
// After the operator rollback to 2,172,037 (cycle-32, 2026-08-06), the
// 2205000/2206004 pins are no longer in the map (those block heights are
// above the new canonical tip and reference non-existent blocks). The new
// highest entry is 2172037.
BOOST_CHECK(Checkpoints::CheckHardened(2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")));
}
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
@@ -21,19 +24,23 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash));
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
BOOST_CHECK(!Checkpoints::CheckHardened(2205000, wrongHash));
BOOST_CHECK(!Checkpoints::CheckHardened(2206004, wrongHash));
BOOST_CHECK(!Checkpoints::CheckHardened(2172037, wrongHash));
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
// pins), so any hash is allowed at those heights.
// pins), and after the cycle-32 operator rollback the 2205000+ pins
// themselves are gone. Any hash is allowed at those heights.
BOOST_CHECK(Checkpoints::CheckHardened(2186940, wrongHash));
BOOST_CHECK(Checkpoints::CheckHardened(2186941, wrongHash));
BOOST_CHECK(Checkpoints::CheckHardened(2205000, wrongHash));
BOOST_CHECK(Checkpoints::CheckHardened(2206004, wrongHash));
BOOST_CHECK(Checkpoints::CheckHardened(42, wrongHash));
}
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
{
BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 2205000);
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returns
// 2,172,037 (the new highest compiled checkpoint).
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2172037);
}
BOOST_AUTO_TEST_SUITE_END()
+25 -16
View File
@@ -680,11 +680,11 @@ BOOST_AUTO_TEST_CASE(reorg_guard_fails_closed_when_checkpoint_pointer_null)
BOOST_CHECK(src.find("nHardenedCheckpointHeight = Checkpoints::GetLastCheckpointHeight()")
!= std::string::npos);
// (c) The guard fires for any fork point at or below the resolved
// (c) The guard fires for any fork point strictly below the resolved
// checkpoint height — independent of whether the resolution came
// from the pointer or the compiled map. The literal pattern that
// matters is `pfork->nHeight <= nHardenedCheckpointHeight`.
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
// matters is `pfork->nHeight < nHardenedCheckpointHeight`.
BOOST_CHECK(src.find("pfork->nHeight < nHardenedCheckpointHeight")
!= std::string::npos);
// (d) The old guard pattern that short-circuited on the null pointer
@@ -699,40 +699,49 @@ BOOST_AUTO_TEST_CASE(reorg_guard_fails_closed_when_checkpoint_pointer_null)
// ─── Off-by-one hardening: guard operator + bootstrap boundary semantics ──
// Adversarial review (Codex round 3 on 6116cff) flagged that the source-grep
// test reorg_guard_fails_closed_when_checkpoint_pointer_null could let through
// a future refactor that weakens the boundary (e.g., changing `<=` to `<`)
// a future refactor that weakens the boundary (e.g., changing `<` to `<=`)
// or splits the guard across files. This test pins:
// (i) the operator used by the guard (must be `<=`),
// (i) the operator used by the guard (must be `<`),
// (ii) the runtime return value of Checkpoints::GetLastCheckpointHeight()
// against the actual compiled map (must equal the highest compiled
// checkpoint height),
// (iii) that the literal RejectReason message uses the "at or below" wording
// (matches `<=`).
// (iii) that the literal RejectReason message uses the "below" wording
// (matches `<`).
//
// Cycle-33 update: the operator is now strict `<` (not `<=`). Reason: a fork
// whose common ancestor EQUALS the checkpoint height preserves the checkpoint
// block (which both chains share) and only replaces blocks AFTER the
// checkpoint. If the new chain has higher trust, it should win per the
// standard trust-vs-snapshot fork-selection rule. Rejecting such a reorg
// would cause honest nodes that observed different height-(checkpoint+1) blocks
// to remain split forever even when they agree on the checkpoint.
BOOST_AUTO_TEST_CASE(reorg_guard_offbyone_hardening)
{
// (i) The guard predicate uses `<=`, NOT `<` or `>=`.
// A regression that introduced `pfork->nHeight < nHardenedCheckpointHeight`
// would let a fork exactly at the checkpoint height through.
// (i) The guard predicate uses `<`, NOT `<=` or `>=`.
// A regression that introduced `pfork->nHeight <= nHardenedCheckpointHeight`
// would cause a permanent chain split when honest nodes see different
// height-(checkpoint+1) blocks.
std::string src = readEntireFile("src/main.cpp");
BOOST_REQUIRE(!src.empty());
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
!= std::string::npos);
BOOST_CHECK(src.find("pfork->nHeight < nHardenedCheckpointHeight")
!= std::string::npos);
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
== std::string::npos);
BOOST_CHECK(src.find("pfork->nHeight >= nHardenedCheckpointHeight")
== std::string::npos);
// (iii) The reject message wording matches `<=` ("at or below").
BOOST_CHECK(src.find("\"REORGANIZE: REJECTED — fork point %d is at or below")
// (iii) The reject message wording matches `<` ("below").
BOOST_CHECK(src.find("\"REORGANIZE: REJECTED — fork point %d is below")
!= std::string::npos);
// (ii) Runtime: GetLastCheckpointHeight() returns the highest compiled
// checkpoint height on mainnet. Verified against the actual binary.
int nCompiled = Checkpoints::GetLastCheckpointHeight();
BOOST_CHECK(nCompiled > 0); // sanity: compiled map populated
// Must equal the highest key in the compiled map (2224763 as of v6.2.6.0;
// Must equal the highest key in the compiled map (2172037 as of cycle-33;
// this assertion locks the value at the time the binary was built, so
// a regression that drops a checkpoint would also fail here).
BOOST_CHECK_EQUAL(nCompiled, 2224763);
BOOST_CHECK_EQUAL(nCompiled, 2172037);
}
// ─── Duplicate-guard detection: variable referenced only in allowed files ─