Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d86a4b77fb | |||
| a99e438895 | |||
| a08162d767 | |||
| 3f0fa3aa8a | |||
| 7bb7a5ef93 | |||
| b70725da36 | |||
| f2978ca389 | |||
| 880474065d | |||
| d0506f9e8b | |||
| 6b2293ad1a | |||
| 0cadbba30c | |||
| fc4bba23b3 | |||
| d72c1ac365 | |||
| 036b7259ad | |||
| 77507e2311 |
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-35
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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!
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user