Compare commits

...

12 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
12 changed files with 763 additions and 788 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
+49 -25
View File
@@ -41,22 +41,40 @@ namespace Checkpoints
// 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
// 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. 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
// 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.
@@ -68,17 +86,23 @@ namespace Checkpoints
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
// 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
// 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 4
#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";
+8 -4
View File
@@ -4424,16 +4424,20 @@ bool FastImportBlockFile()
hash.ToString().c_str());
}
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
// Insert into mapBlockIndex and set phashBlock BEFORE calling
// GetStakeModifierChecksum, which calls GetBlockHash() which
// dereferences phashBlock. Without this ordering, phashBlock is
// null and the checksum call segfaults.
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &mi->first;
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
// PoS stake seen set
if (pindexNew->IsProofOfStake())
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
// Insert into mapBlockIndex
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &mi->first;
// pnext is rebuilt after best-chain selection. File order also
// contains side branches, so assigning it here would let the last
// imported child hijack stake-modifier forward walks.
+54 -21
View File
@@ -17,12 +17,38 @@
#include <QCheckBox>
#include <QApplication>
#include <filesystem>
#include <cstdio>
#include <ctime>
#include <set>
#include <filesystem>
namespace fs = std::filesystem;
// Convert QString to fs::path preserving non-ASCII characters on Windows.
// On Windows, QString::toStdString() returns UTF-8 but std::filesystem::path
// constructed from a narrow string then uses the ANSI code page, which
// mangles UTF-8 paths. QString::toStdWString() + fs::path(std::wstring)
// preserves them. On non-Windows platforms the UTF-8 path is correct.
static fs::path qstringToPath(const QString& s)
{
#ifdef WIN32
return fs::path(std::wstring(s.toStdWString()));
#else
return fs::path(s.toStdString());
#endif
}
// 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)
{
@@ -157,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 {
@@ -216,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));
@@ -232,7 +262,7 @@ bool IntroDialog::pickDataDirectory()
// Auto-bootstrap: if no blockchain data exists, download automatically.
// If data exists, offer optional re-download (unless user checked "don't ask again").
fs::path dataDirPath(dataDir.toStdString());
fs::path dataDirPath = qstringToPath(dataDir);
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataDirPath);
bool userWantsBootstrap = false;
// Captured local-load error from the staged-snapshot probe below, surfaced
@@ -429,21 +459,24 @@ bool IntroDialog::pickDataDirectory()
QApplication::processEvents();
};
// Try the fast UTXO snapshot path first (matches daemon behavior in init.cpp).
// The legacy DownloadBootstrap() is hard-disabled in bootstrap.cpp — it always
// returns false with "Legacy file-list bootstrap is disabled". Calling it here
// would make the GUI wallet unable to bootstrap a fresh install.
// Try the fast UTXO snapshot path. The GUI has already probed the data
// dir for a staged utxo-snapshot.bin above; if that didn't find one,
// DownloadUtxoSnapshot is the canonical HTTPS path to the bootstrap
// server. TLS validation is now handled in bootstrap.cpp's StartTLS
// via a layered trust store (exedir cacert.pem → SSL_CERT_FILE →
// system default paths → embedded ISRG X1 + X2 as belt-and-suspenders),
// so this should succeed on Windows GUI builds where the Qt-bundled
// libssl-3-x64.dll ships without a default cert path. The system-path
// call is trusted on its return value (OpenSSL's hashed-directory
// lookups are lazy and would otherwise show 0 eagerly loaded store
// objects even on a valid install); the embedded fallbacks are
// always attempted as cross-sign resilience and become load-bearing
// on a stripped Windows GUI with no cacert.pem and no usable system
// CA directory.
std::string utxoError;
bool success = Bootstrap::DownloadUtxoSnapshot(host, dataDirPath, progressFn, utxoError);
if (!success) {
// Fall back to legacy bootstrap path (will fail with "disabled" error, but
// surfaces the real error if the snapshot path had a different failure).
std::string legacyError;
if (Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, legacyError)) {
success = true;
} else {
strError = "UTXO snapshot: " + utxoError + " | Legacy: " + legacyError;
}
strError = utxoError;
}
if (!success) {
// The TLS-detection strings are matched against the standard error
@@ -509,8 +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;
+40 -7
View File
@@ -13,9 +13,12 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain)
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
// 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.
// 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)
@@ -25,10 +28,14 @@ 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(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), and after the cycle-32 operator rollback the 2205000+ pins
// themselves are gone. 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));
@@ -38,9 +45,35 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
{
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returns
// 2,172,037 (the new highest compiled checkpoint).
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2172037);
// 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()
+5 -4
View File
@@ -738,10 +738,11 @@ BOOST_AUTO_TEST_CASE(reorg_guard_offbyone_hardening)
// 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 (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, 2172037);
// 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);
}
}
}