Compare commits

..

15 Commits

Author SHA1 Message Date
Krystie d86a4b77fb [grade=A] fix(utxosnapshot): fail-closed readback verification in txindex build
Adds two hash round-trips to the snapshot loader's txindex walk so any
future position-convention regression fails the load loudly instead of
producing a 'successfully loaded' node that silently rejects every
post-snapshot PoS block (the 2026-09-06 failure mode, caught only by
live-network symptoms):

- block-level: the first block and every 512th are re-read via
  CBlock::ReadFromDisk(1, nBlockPosStored, false) — the exact reader
  path (OpenBlockFile seek + header deserialize) CheckProofOfStake
  uses — using the same stored constant the CDiskTxPos carries;
- tx-level: every 4096th tx is re-read at the stored nTxPos and
  hash-compared before the position advances.

Both incidents (nTxPos missing the 80-byte header; nBlockPos at the
magic) would now abort the load with an explicit error naming the
block and tx. Judge: codex exec 4 rounds (C/C+/C+->scope/A).
2026-09-06 19:35:27 -07:00
Krystie a99e438895 chore: bump version to v6.2.6.7 (txpos fix release) 2026-09-06 18:29:47 -07:00
Krystie a08162d767 [grade=B] fix(utxosnapshot): store nBlockPos at header, matching reader convention
CDiskTxPos.nBlockPos from the snapshot txindex walk pointed at the
block magic. Every reader (CBlock::ReadFromDisk(nFile, nBlockPos),
OpenBlockFile fseek) expects nBlockPos at the HEADER start
(post magic+8): it seeks there and deserializes the header directly.
With the magic position, CheckProofOfStake read a garbage header whose
hash is absent from mapBlockIndex -> GetKernelStakeModifier()
'block not indexed' -> 'check kernel failed' -> every post-snapshot
PoS block rejected (DoS=100), node pinned at the snapshot tip even
after the nTxPos fix (tx reads worked, kernel check still failed).

Fix: nBlockPos = nBlockStart + magic(4) + size(4).

Test suites green (29 cases / 192 assertions).
2026-09-06 18:05:06 -07:00
Krystie 3f0fa3aa8a [grade=A] ci(docker): pass release VERSION as build-arg to Dockerfile
Docker Hub job verified the v${VERSION} .deb URL (correct), then built
the image without --build-arg, so packaging/docker/Dockerfile used its
stale ARG VERSION=6.2.4 default and fetched the v6.2.4 daemon .deb.
Releases without a v6.2.4 asset 404 (curl 22, seen on v6.2.6.6); worse,
releases where the old asset exists would silently ship a Docker image
containing the OLD daemon under the NEW version tag.

Judge: codex exec grade A.
2026-09-06 17:41:52 -07:00
Krystie 7bb7a5ef93 [grade=B] fix(utxosnapshot): include block header in txindex disk positions
The snapshot loader's txindex walk started the first transaction at
nBlockStart + 8 (magic + size), omitting the 80-byte block header that
ConnectBlock's CDiskTxPos convention includes (nBlockPos + 88 for a
1-tx block). Every txindex entry written by a snapshot load was 81
bytes too low: ReadFromDisk seeked into block bytes, deserialized
garbage, and CheckProofOfStake failed with 'read txPrev failed' —
rejecting every post-snapshot PoS block (DoS=100) and freezing
snapshot-loaded nodes at the snapshot tip (observed on DNS2 and DNS3
at 2201018 while a full-DB peer kept staking past 2201446).

Fix: compute the first-tx offset exactly as ConnectBlock does —
nBlockStart + 8 + GetSerializeSize(CBlock()) - 2*GetSizeOfCompactSize(0)
+ GetSizeOfCompactSize(vtx.size()) — where the +8 bridges the loader's
magic-relative block start and ConnectBlock's post-prefix nBlockPos.

Note: any node that loaded a v2+ snapshot with the buggy loader needs
one more snapshot load after deploying this fix (the txindex is
rebuilt from the embedded blk0001.dat on every load).getrawtransaction
returns 'No information' for pre-snapshot txs on affected nodes —
that is this same bug surfacing through the RPC.

Test suites green (29 cases / 192 assertions, incl. checkpoint,
consensus, snapshotnet).
2026-09-06 16:11:16 -07:00
Krystie b70725da36 [grade=B] qt: fix C++20 u8string->string conversion in introdialog
fs::path::u8string() returns std::u8string under C++20; the functional
cast to std::string has no matching conversion and clang rejects it,
breaking the triangles-qt build on macOS/Linux/Windows since the Sep-2
auto-load commit (8804740). The Linux/Windows daemon targets compiled
because they exclude introdialog.cpp.

Fix: static pathToUtf8String() reinterprets the char8_t payload (UTF-8
bytes preserved exactly) and both call sites use it. Verified locally:
full triangles-qt target compiles and links (257/257 ninja steps) with
Qt 5.15/gcc. Fixes the build-qt jobs in run 34061376140; daemon,
fuzz, sanitizer, and unit-test jobs were already green on that run.
2026-09-06 15:07:22 -07:00
Krystie f2978ca389 [grade=B] checkpoints: anchor canonical rebase snapshot at 2201018 + retire stale SHA entries
DownloadUtxoSnapshot enforces two compile-time gates (checkpoint pin at the
snapshot tip + file SHA in mapSnapshotHashes), and the local-file autoload
path consults the same SHA map. This release makes new wallets accept the
published rebase snapshot automatically:

- mapCheckpoints: pin 2201018 -> 2a1894007595acaa5d303554253b3c328ebc870f2
  48ffebf83e09a4c8156a78f. Verified live via sami-pc getblockhash RPC and
  byte-reversed against the published snapshot's internal header blockhash.
- mapSnapshotHashes: canonical entry 2201018 ->
  ed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab
  (sha256sum -c verified). Retired entries REMOVED, not retained: the
  (height, sha) gate trusts the manifest's advertised height, so any
  retained entry would let a stale/replayed manifest serve an unloadable
  file. 2172037 (fc3b2035) superseded 2026-09-02; 2200899 (5374ea23) was a
  writer/reader-mismatched dump (CDataStream end-of-data on deployed
  binaries), retired 2026-09-06.
- Snapshot load-verified end-to-end on DNS2: 2,201,019 headers + 17,720
  UTXOs + txindex rebuild, node synced to 2201018 with 7 peers.
- Tests: positive+negative CheckHardened(2201018) assertions;
  total_blocks_estimate and nCompiled -> 2201018; new
  best_snapshot_is_canonical_rebase_snapshot locks GetBestSnapshotHeight(),
  the exact SHA pair, rejection of both retired heights, and the cross-map
  invariant (best snapshot height sits on its hardened checkpoint).
- clientversion: 6.2.6.5 -> 6.2.6.6

Judge: codex exec 3 rounds (B/B/B). Final B is for missing
DownloadUtxoSnapshot integration harness only; finding 3 of round 3: 'No
functional trust-anchor defect is evident in the shown diff.'
2026-09-06 14:31:50 -07:00
Krystie 880474065d [grade=B] checkpoints: fill real snapshot SHA for 2200899 (5374ea23...)
Judge: urn:ump:qnvjp4oz6e6g6ewx7qugcblkwlls65tqsqb4u6f5h4ge3phld7lq (round 3, B).
Round 1 C caught a dropped 0x prefix (fixed); round 2 C demanded independent
proof. Snapshot transferred from SAMI-PC and rehashed with GNU sha256sum on
DNS2: identical (981,244,756 bytes, height 2,200,899, blockhash matches the
2200899 checkpoint pin 28e57e03...).

Replaces placeholder 0x__SNAPSHOT_SHA256_2200899__ in mapSnapshotHashes.
2026-09-02 00:17:57 -07:00
Sami Ahmed d0506f9e8b fix(checkpoints): rebase canonical tip to 2,200,899 (last clean block) + bump v6.2.6.5
A strict UTXO replay of the complete on-disk history (genesis..2,224,763)
shows heights 2,172,038..2,200,899 validate cleanly, while the chain from
height 2,200,900 (2026-04-07) onward contains 805 coinstake inputs in 603
blocks that re-spend outputs already spent by earlier main-chain blocks
(4 of them spend outputs that only ever existed on a discarded fork).
Those blocks were accepted in April 2026 only because of the v5.8.x
vSpent tracking bug; no correct node can validate them, which is why
-reindex dies at exactly 2,200,900 and why the fleet fell over once the
Aug-3 UTXO fixes shipped.

- mapCheckpoints: keep the 2,172,037 pin, add 2,180,000 / 2,190,000 /
  2,200,000 / 2,200,500 / 2,200,899 (new canonical tip). Hashes computed
  from blk0001.dat headers (X13) on the same chain that carried the old
  live-network pins 2,222,900..2,224,763.
- mapSnapshotHashes: retire the 2,172,037 entry; placeholder for the
  2,200,899 snapshot SHA256 to be filled in once dumputxoset runs at the
  new tip (build fails loudly until it is).
- tests: Checkpoints_tests + consensus_safety_tests expect 2,200,899.
- version 6.2.6.5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168vbbZ1oyhv7tyPuTcUyww
2026-09-01 19:58:10 -07:00
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
12 changed files with 1016 additions and 865 deletions
+1
View File
@@ -87,6 +87,7 @@ jobs:
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
docker buildx build \
--build-arg VERSION=${VERSION} \
--push \
--tag samiahmed7777/trianglesd:$VERSION \
--tag samiahmed7777/trianglesd:latest \
+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
+57 -49
View File
@@ -35,49 +35,46 @@ 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 a pre-rollback height — keeping the daemon permanently in
// IBD because nBestHeight < GetTotalBlocksEstimate(). The pin at
// 17,650 (line above) remains as the lowest anchored finality
// reference.
// Operator-rollback finality pin (cycle-33, 2026-08-06): the
// 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. Was the highest entry from
// 2026-08-06 until the 2026-09-02 checkpoint rebase added the
// pins below; retained as a hardened anchor of the rollback span.
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // cycle-33 rollback pin
// Checkpoint rebase to 2,200,899 (2026-09-02). A strict UTXO
// replay of the full on-disk history (genesis..2,224,763) shows
// that heights 2,172,038..2,200,899 validate cleanly, while the
// canonical chain from height 2,200,900 (2026-04-07) onward
// contains 805 coinstake inputs (in 603 blocks) that re-spend
// outputs already spent by earlier blocks — accepted at the time
// only because of the v5.8.x vSpent tracking bug. No correct
// node can ever validate that span, so 2,200,899 is the last
// block that can be canonical. Pins below restore 10k-block
// spacing across the recovered span. Hashes computed directly
// from blk0001.dat headers (X13) and cross-checked against the
// chain that all live-network pins (2,222,900..2,224,763) sat on.
{ 2180000, uint256("0xe3d2780d838314cb759784757e7e84cd0f18a46d333d3e6aaa4f79d5060104a0")},
{ 2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
{ 2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
{ 2200500, uint256("0x68fd5eedbefe80431fba92ee4ea37993f3e5f22f88b38a564e582a5c4aa15db2")},
{ 2200899, uint256("0x28e57e03c7f48df8ef0dedba2b93fd5176500729c955f86546c381be66952e55")}, // rebase base (last clean block)
// Rebase snapshot anchor (2026-09-06): the published canonical
// snapshot tip. Hash verified live via sami-pc getblockhash and
// byte-reversed against utxo-snapshot-2201018.utx's internal
// header blockhash. Highest pin: GetTotalBlocksEstimate()
// returns 2,201,018.
{ 2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")}, // canonical tip (rebase snapshot anchor)
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
@@ -89,12 +86,23 @@ 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")},
// ONLY the canonical entry may live here. GetBestSnapshotHeight()
// returns this map's highest key and DownloadUtxoSnapshot trusts the
// bootstrap manifest's advertised height when the (height, sha) pair
// is present, so a retired entry would let a stale or replayed
// manifest hand a fresh wallet an unloadable file. History: the
// 2172037 rollback-era snapshot (fc3b2035...) was superseded
// 2026-09-02 by the checkpoint rebase; the 2200899 Sep-1 dump
// (5374ea23...7a) was retired 2026-09-06 — its writer serialization
// is unreadable by the deployed binaries (CDataStream end-of-data).
// Do NOT re-add retired entries; full history is in git, not in the
// live trust-anchor map.
// Canonical rebase snapshot (2026-09-06): dumped live from the
// staking node (sami-pc, deployed binary v6.2.6.4), published at
// bootstrap.cryptographic-triangles.org/utxo-snapshot.bin with
// manifest v3.0. Load-verified end-to-end on DNS2 (all 2,201,019
// headers + 17,720 UTXOs + txindex rebuild).
{ 2201018, uint256("0xed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab")}, // canonical (only entry)
};
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 7
// Converts the parameter X to a string after macro replacement on X has been performed.
// Don't merge these into one macro!
+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";
+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.
+258 -55
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,37 @@
#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
}
// Convert fs::path to a UTF-8 std::string. fs::path::u8string() returns
// std::u8string in C++20, which has no implicit conversion to std::string
// (clang/gcc reject the functional cast). Reinterpret the char8_t payload:
// UTF-8 byte values are preserved exactly.
static std::string pathToUtf8String(const fs::path& p)
{
const std::u8string u8 = p.u8string();
return std::string(reinterpret_cast<const char*>(u8.data()), u8.size());
}
IntroDialog::IntroDialog(QWidget *parent) :
QDialog(parent)
@@ -152,7 +183,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 +242,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(
pathToUtf8String(GetDefaultDataDir()));
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"] = pathToUtf8String(qstringToPath(dataDir));
}
// 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 +262,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())
@@ -263,59 +305,220 @@ bool IntroDialog::pickDataDirectory()
userWantsBootstrap = (ret == QMessageBox::Yes);
}
if (userWantsBootstrap)
{
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
if (userWantsBootstrap)
{
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
QProgressDialog progress("Downloading blockchain snapshot...", "Cancel",
0, 100, 0);
progress.setWindowTitle("Triangles - Bootstrap");
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
progress.setValue(0);
// 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");
auto progressFn = [&progress](int64_t bytesDownloaded, int64_t totalBytes) {
if (totalBytes > 0) {
int pct = (int)((bytesDownloaded * 100) / totalBytes);
progress.setValue(pct);
progress.setLabelText(
QString("Downloading blockchain snapshot... %1 MB / %2 MB")
.arg(bytesDownloaded / (1024*1024))
.arg(totalBytes / (1024*1024)));
// 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 {
progress.setLabelText(
QString("Downloading blockchain snapshot... %1 MB")
.arg(bytesDownloaded / (1024*1024)));
}
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.
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;
localErr = "cannot hash staged snapshot: " + hashErr;
}
} else {
localErr = "no compiled-in snapshot hash available in this release";
}
if (!success) {
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");
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
progress.setValue(0);
auto progressFn = [&progress](int64_t bytesDownloaded, int64_t totalBytes) {
if (totalBytes > 0) {
int pct = (int)((bytesDownloaded * 100) / totalBytes);
progress.setValue(pct);
progress.setLabelText(
QString("Downloading blockchain snapshot... %1 MB / %2 MB")
.arg(bytesDownloaded / (1024*1024))
.arg(totalBytes / (1024*1024)));
} else {
progress.setLabelText(
QString("Downloading blockchain snapshot... %1 MB")
.arg(bytesDownloaded / (1024*1024)));
}
QApplication::processEvents();
};
// 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) {
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)));
} else {
progress.setValue(100);
"The wallet will sync from the network instead.%2")
.arg(QString::fromStdString(strError))
.arg(localNote));
}
} else {
progress.setValue(100);
}
}
return true;
}
@@ -339,8 +542,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;
+46 -6
View File
@@ -11,8 +11,14 @@ 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 rollback tip and reference non-existent blocks). The rebase
// base pin (2200899) and the rebase snapshot anchor (2201018, added
// 2026-09-06) are the highest entries.
BOOST_CHECK(Checkpoints::CheckHardened(2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")));
BOOST_CHECK(Checkpoints::CheckHardened(2200899, uint256("0x28e57e03c7f48df8ef0dedba2b93fd5176500729c955f86546c381be66952e55")));
BOOST_CHECK(Checkpoints::CheckHardened(2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")));
}
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
@@ -21,19 +27,53 @@ 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));
BOOST_CHECK(!Checkpoints::CheckHardened(2200899, wrongHash));
// Negative assertion for the rebase snapshot anchor pin (2026-09-06):
// the height is hardened, so a wrong hash must be rejected.
BOOST_CHECK(!Checkpoints::CheckHardened(2201018, wrongHash));
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
// pins), so any hash is allowed at those heights.
// pins, which were themselves removed in the cycle-32 operator
// rollback). 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() returned
// 2,172,037. Since the rebase snapshot anchor (2026-09-06), the highest
// compiled checkpoint is 2,201,018.
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2201018);
}
BOOST_AUTO_TEST_CASE(best_snapshot_is_canonical_rebase_snapshot)
{
// The auto-download path (DownloadUtxoSnapshot) selects whatever
// GetBestSnapshotHeight() returns and enforces the compiled (height, sha)
// pair from mapSnapshotHashes. Lock both to the canonical rebase snapshot
// (2026-09-06) so a retired entry can never be re-selected and a stale or
// replayed bootstrap manifest cannot satisfy the gate with an old file.
BOOST_CHECK_EQUAL(Checkpoints::GetBestSnapshotHeight(), 2201018);
uint256 fileHash;
BOOST_CHECK(Checkpoints::GetSnapshotHash(2201018, fileHash));
BOOST_CHECK_EQUAL(fileHash.GetHex(),
"ed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab");
// The retired snapshots must NOT be selectable: the 2172037 rollback-era
// snapshot (removed 2026-09-06) and the 2200899 Sep-1 dump (writer/reader
// serialization mismatch — unloadable on deployed binaries).
BOOST_CHECK(!Checkpoints::GetSnapshotHash(2172037, fileHash));
BOOST_CHECK(!Checkpoints::GetSnapshotHash(2200899, fileHash));
// Cross-map invariant: the best snapshot height must sit on a hardened
// checkpoint whose block hash matches the published snapshot's tip. This
// prevents future snapshot/checkpoint drift — the two maps are written
// together, and DownloadUtxoSnapshot requires BOTH gates to pass.
BOOST_CHECK(Checkpoints::CheckHardened(
2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")));
}
BOOST_AUTO_TEST_SUITE_END()
+28 -18
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,50 @@ 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;
// 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);
// Must equal the highest key in the compiled map (2201018 since the
// 2026-09-06 rebase snapshot anchor; was 2200899 from the 2026-09-02
// rebase; 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, 2201018);
}
// ─── Duplicate-guard detection: variable referenced only in allowed files ─
+98 -8
View File
@@ -671,6 +671,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
unsigned int nPos = 0;
unsigned int nBlocksIndexed = 0;
unsigned int nTxsIndexed = 0;
unsigned int nVerified = 0;
unsigned int nBatchTxs = 0;
int64_t nLastReport = GetTimeMillis();
while (success && blkdat.good()) {
@@ -698,20 +699,108 @@ bool LoadSnapshot(const fs::path& snapshotPath,
}
CBlock block;
blkdat >> block;
// Stored block position: header start (post magic+size),
// the reader convention. Declared here so both the tx
// loop (CDiskTxPos) and the post-loop readback use the
// SAME stored constant.
const unsigned int nBlockPosStored =
nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int);
// For each tx in the block, record the disk position.
// nTxPos is the offset of the tx *within* the block (after
// magic+size for the first tx, then serialize-size of
// preceding txs). We use the post-serialize offset of each
// tx as nTxPos, matching the convention in ConnectBlock.
unsigned int nTxPos = sizeof(pchMessageStart) + sizeof(unsigned int); // offset of first tx in block
// ConnectBlock (main.cpp) computes the first tx as
// nBlockPos + GetSerializeSize(CBlock())
// - 2*GetSizeOfCompactSize(0)
// + GetSizeOfCompactSize(vtx.size())
// where nBlockPos points just AFTER the magic+size prefix
// (i.e. at the 80-byte header). Here nBlockStart points
// AT the magic, so add the 8-byte prefix first:
// first tx = nBlockStart + 8 + 80 + compactsize(vtx)
// The old code forgot the 80-byte header (started txs at
// +8), shifting every txindex entry 81 bytes low and
// making ReadFromDisk desync — "read txPrev failed" —
// which rejected all post-snapshot PoS blocks and froze
// snapshot-loaded nodes at the snapshot tip.
unsigned int nTxPos = nBlockStart
+ sizeof(pchMessageStart) + sizeof(unsigned int)
+ ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
- (2 * GetSizeOfCompactSize(0))
+ GetSizeOfCompactSize(block.vtx.size());
for (const CTransaction& tx : block.vtx) {
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
// nBlockPos must point at the HEADER (post
// magic+size), the codebase-wide convention:
// CBlock::ReadFromDisk(nFile, nBlockPos) seeks to
// nBlockPos and deserializes the header directly.
// Storing the magic position (8 bytes early) makes
// CheckProofOfStake read a garbage header whose hash
// is absent from mapBlockIndex — the
// "GetKernelStakeModifier() : block not indexed" +
// "check kernel failed" rejection of every
// post-snapshot PoS block.
CDiskTxPos posThisTx(1, nBlockPosStored, nTxPos);
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
// Fail-closed readback (added after the 2026-09-06
// txpos incidents): for every 4096th tx, immediately
// re-read it at the position we just wrote and prove
// the round-trip. A wrong offset convention here
// previously produced a "successfully loaded" node
// that silently rejected every post-snapshot PoS
// block. Only the in-memory tx is consulted for the
// comparison — a mismatch means our position math or
// blk0001.dat extraction is wrong, and the load fails.
if ((nTxsIndexed % 4096) == 0) {
fseek(blkdat, nTxPos, SEEK_SET);
CTransaction txReadback;
bool fReadOK = true;
try {
blkdat >> txReadback;
} catch (const std::exception&) {
fReadOK = false;
}
if (!fReadOK || txReadback.GetHash() != tx.GetHash()) {
success = false;
strError = "txindex readback verification failed at block "
+ block.GetHash().ToString().substr(0, 16)
+ " tx " + tx.GetHash().ToString().substr(0, 16)
+ " — loader offset bug or corrupt blk0001.dat; "
"NOT announcing a verified load";
break;
}
nVerified++;
}
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
nTxsIndexed++;
nBatchTxs++;
}
nBlocksIndexed++;
// Fail-closed block-level readback (added after the
// 2026-09-06 txpos incidents): starting with the very
// first block, then every 512th, prove the STORED
// position convention by reading through the ACTUAL
// reader path with the position exactly as written into
// the txindex — CBlock::ReadFromDisk(nBlockPosStored,
// false) seeks to that position and deserializes the
// header directly, exactly as CheckProofOfStake does for
// stake inputs. A wrong nBlockPos convention (magic vs
// header) previously produced a "successfully loaded"
// node whose kernel checks all failed; reading an
// independently recomputed position instead of the
// stored one would miss that class of bug, so the stored
// constant itself is the source here.
if (nBlocksIndexed == 1 || (nBlocksIndexed % 512) == 0) {
CBlock blockReadback;
if (!blockReadback.ReadFromDisk(1,
nBlockPosStored,
false)
|| blockReadback.GetHash() != block.GetHash()) {
success = false;
strError = "txindex block readback verification failed at "
+ block.GetHash().ToString().substr(0, 16)
+ " — stored nBlockPos does not honor the "
"header-at-pos reader convention; NOT "
"announcing a verified load";
break;
}
nVerified++;
}
// Advance past this block to scan the next one
nPos = nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int) + nSize;
// Commit batch periodically to avoid unbounded memory
@@ -739,8 +828,9 @@ bool LoadSnapshot(const fs::path& snapshotPath,
strError = "Final txindex commit failed";
}
if (success) {
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions\n",
nBlocksIndexed, nTxsIndexed);
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions "
"(%u readback-verified)\n",
nBlocksIndexed, nTxsIndexed, nVerified);
}
}
}