Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b2293ad1a | |||
| 0cadbba30c | |||
| fc4bba23b3 | |||
| d72c1ac365 | |||
| 036b7259ad | |||
| 77507e2311 | |||
| 019f5f33be | |||
| 66ac7e8537 | |||
| 93f879583f | |||
| b13139ad19 | |||
| d35aec1828 | |||
| 53a2f0b5d2 | |||
| 9cb44a2988 | |||
| c37102eff4 | |||
| d0e3657014 | |||
| cb397b6be9 | |||
| a1fae5f6f3 | |||
| 717f0d07cd |
@@ -670,7 +670,16 @@ if(BUILD_TESTS)
|
|||||||
Boost::unit_test_framework
|
Boost::unit_test_framework
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}: the consensus_safety_tests
|
||||||
|
# `reindex_reconstruction_is_explicit_and_fail_closed` test reads
|
||||||
|
# src/init.cpp + src/main.cpp via __FILE__-relative path traversal
|
||||||
|
# (3x parent_path() calls). When ctest runs from build/src/ (the
|
||||||
|
# default CMAKE_CURRENT_BINARY_DIR for src/CMakeLists.txt), the
|
||||||
|
# resolved path is build/src/src/init.cpp which doesn't exist.
|
||||||
|
# Pinning WORKING_DIRECTORY to "${CMAKE_SOURCE_DIR}" makes the test
|
||||||
|
# source paths resolve correctly from any environment.
|
||||||
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
|
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
|
||||||
|
set_tests_properties(triangles_unit_tests PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
# ── Standalone chaindb equivalence tests ─────────────────────────────────
|
# ── Standalone chaindb equivalence tests ─────────────────────────────────
|
||||||
# Runs without the TestingSetup global fixture (which would otherwise
|
# Runs without the TestingSetup global fixture (which would otherwise
|
||||||
|
|||||||
+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
|
// Check if data dir already has blockchain data
|
||||||
bool NeedsBootstrap(const std::filesystem::path& dataDir);
|
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
|
// If noProxy is true, bypass Tor SOCKS proxy and connect directly
|
||||||
// (used for clearnet bootstrap downloads).
|
// (used for clearnet bootstrap downloads).
|
||||||
// If portOverride is set (>0), uses that port instead of the default PORT.
|
// If portOverride is set (>0), uses that port instead of the default PORT.
|
||||||
@@ -35,19 +35,6 @@ namespace Bootstrap {
|
|||||||
int portOverride = -1,
|
int portOverride = -1,
|
||||||
int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024);
|
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.
|
// Advertised identity of a snapshot listed by manifest.json.
|
||||||
// The advertised SHA256 is accepted only when it matches the hash compiled
|
// The advertised SHA256 is accepted only when it matches the hash compiled
|
||||||
// into checkpoints.cpp for the same height.
|
// into checkpoints.cpp for the same height.
|
||||||
@@ -59,31 +46,12 @@ namespace Bootstrap {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Parse and validate the small, untrusted bootstrap manifest. This routine
|
// Parse and validate the small, untrusted bootstrap manifest. This routine
|
||||||
// performs no network I/O and is exposed so malformed-input behavior can be
|
// performs no network I/O and is exposed so malformed-input behavior can
|
||||||
// covered by unit tests.
|
// be covered by unit tests.
|
||||||
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
|
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
|
||||||
RemoteSnapshot& snapshot,
|
RemoteSnapshot& snapshot,
|
||||||
std::string& strError);
|
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.
|
// Download a UTXO snapshot and load it into a fresh txleveldb.
|
||||||
// This is much faster than downloading the full bootstrap archive.
|
// This is much faster than downloading the full bootstrap archive.
|
||||||
// Returns true if snapshot was downloaded and loaded successfully.
|
// 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
|
||||||
+33
-49
@@ -35,49 +35,28 @@ namespace Checkpoints
|
|||||||
// Recent finality pin (PoS era). Closes the long unchecked span from
|
// Recent finality pin (PoS era). Closes the long unchecked span from
|
||||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||||
// this height are rejected outright. Hash from the canonical chain.
|
// this height are rejected outright. Hash from the canonical chain.
|
||||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
// Operator rollback canonical (cycle-32, 2026-08-06): the chain was
|
||||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
// rolled back to height 2,172,037 (hash 52b12f09...) so the entire
|
||||||
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
|
// span 2,172,038..2,224,763 no longer exists on the canonical chain.
|
||||||
// gap between the last hardcoded checkpoint and the live tip stays bounded.
|
// All pins from 2,205,000..2,224,763 have been REMOVED from the map
|
||||||
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
|
// (NOT preserved). Their block hashes are not in the canonical chain,
|
||||||
// unverified blocks at tip — a peer feeding fork blocks at those heights
|
// so leaving them as map entries would let GetTotalBlocksEstimate()
|
||||||
// could trick an IBD node into accepting a divergent chain. With these
|
// return 2,214,400 — keeping the daemon permanently in IBD because
|
||||||
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
|
// nBestHeight (2,172,037) < 2,214,400. With the operator-rollback
|
||||||
// All hashes verified against the canonical chain on 2026-07-01.
|
// pin at 2,172,037 as the new highest entry, GetTotalBlocksEstimate()
|
||||||
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
|
// and GetLastCheckpointHeight() both return 2,172,037, so a node
|
||||||
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
|
// that reaches 2,172,037 exits IBD cleanly. The pin at 17,650
|
||||||
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
|
// (line above) remains as the lowest anchored finality reference.
|
||||||
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
|
// Operator-rollback finality pin (cycle-33, 2026-08-06): the new
|
||||||
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
|
// canonical tip after the operator rollback to 2,172,037. Hash
|
||||||
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
|
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
|
||||||
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
|
// at canonical tip 2,172,037. This is now the highest entry in
|
||||||
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
|
// mapCheckpoints, so GetTotalBlocksEstimate() returns 2,172,037 and
|
||||||
// Post-rebuild finality pin (v6.2.5.0). Closes the gap between
|
// IsInitialBlockDownload() returns false once a node reaches
|
||||||
// the last hardcoded checkpoint and the live tip after -rebuildutxo.
|
// 2,172,037. Closes the unchecked span between the prior highest
|
||||||
// Hash from the canonical chain on DNS2 after fresh UTXO rebuild.
|
// pin (17,650) and the new canonical tip for any future
|
||||||
{ 2219922, uint256("0x9ed3e1d38317950927f37f2867e3fc29e239fc1f4c57b182f55c6e04b73b52ec")},
|
// fresh-from-zero sync.
|
||||||
// Live-tip finality pins (v6.2.6.0). Verified against DNS3 chain state
|
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // new canonical tip
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||||
@@ -89,12 +68,17 @@ namespace Checkpoints
|
|||||||
// here. The corresponding (height, blockhash) must already exist in
|
// here. The corresponding (height, blockhash) must already exist in
|
||||||
// mapCheckpoints / mapCheckpointsTestnet.
|
// mapCheckpoints / mapCheckpointsTestnet.
|
||||||
static std::map<int, uint256> mapSnapshotHashes = {
|
static std::map<int, uint256> mapSnapshotHashes = {
|
||||||
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
|
// Historical snapshots preserved as documentation only. The canonical
|
||||||
{ 2219922, uint256("0x6dd8d782a04bb8dc4ccd5e88a4bc7726fe26bdebaed96b79242de1e2949b6ee6")},
|
// chain is now at 2,172,037 (operator rollback 2026-08-06). Any wallet
|
||||||
// Live-tip snapshot (v6.2.6.0). Generated from DNS3 (Samihost) at the
|
// recovering from these old snapshots would also need to bypass the
|
||||||
// canonical tip 2,224,763, blockhash 9d3575ac...06674. Verified against
|
// chain-state checks via the rollback recipe (see
|
||||||
// the canonical chain on 2026-08-04.
|
// genesis-block-pow-exemption SKILL.md "SAMI-PC wallet recovery recipe"),
|
||||||
{ 2224763, uint256("0xa7ea62ad4e158faf07973e5cd1539c1895154c4e28685a3eb7af458a001037b7")},
|
// which uses the local-file path (utxo-snapshot.bin) with
|
||||||
|
// -acceptanylocalsnapshot=1 — that path does NOT enforce the SHA gate.
|
||||||
|
// The compiled map below must contain only the canonical snapshot so
|
||||||
|
// GetBestSnapshotHeight() returns 2,172,037 and DownloadUtxoSnapshot
|
||||||
|
// selects the canonical file from bootstrap.cryptographic-triangles.org.
|
||||||
|
{ 2172037, uint256("0xfc3b2035525564156f2489e8929e132b75e9be285d9129ad21bc89ecdc4c7977")}, // canonical
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
#define CLIENT_VERSION_MAJOR 6
|
#define CLIENT_VERSION_MAJOR 6
|
||||||
#define CLIENT_VERSION_MINOR 2
|
#define CLIENT_VERSION_MINOR 2
|
||||||
#define CLIENT_VERSION_REVISION 6
|
#define CLIENT_VERSION_REVISION 6
|
||||||
#define CLIENT_VERSION_BUILD 0
|
#define CLIENT_VERSION_BUILD 4
|
||||||
|
|
||||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||||
// Don't merge these into one macro!
|
// Don't merge these into one macro!
|
||||||
|
|||||||
+345
-28
@@ -401,22 +401,53 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
{
|
{
|
||||||
if (running.load()) return true;
|
if (running.load()) return true;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// PHASE 0: validate input BEFORE any state mutation.
|
||||||
|
// If validation fails, we must leave the system in a clean state
|
||||||
|
// (running=false, no i2p data dir side effects, no InitI2P call).
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (socks < 1 || socks > 65535) {
|
||||||
|
lastError = strprintf("SOCKS proxy port %d out of range (1-65535)", socks);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sam < 1 || sam > 65535) {
|
||||||
|
lastError = strprintf("SAM bridge port %d out of range (1-65535)", sam);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (server < 0 || server > 65535) {
|
||||||
|
lastError = strprintf("server tunnel port %d out of range (0-65535, 0=disable)", server);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lastError.clear();
|
lastError.clear();
|
||||||
socksPort = socks;
|
socksPort = socks;
|
||||||
samPort = sam;
|
samPort = sam;
|
||||||
serverPort = server;
|
serverPort = server;
|
||||||
i2pHostname.clear();
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||||
|
i2pHostname.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare i2pd data directory under the wallet's data dir
|
// Prepare i2pd data directory under the wallet's data dir
|
||||||
i2pDataDir = (::GetDataDir() / "i2p_data").string();
|
i2pDataDir = (::GetDataDir() / "i2p_data").string();
|
||||||
fs::create_directories(i2pDataDir);
|
try {
|
||||||
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
fs::create_directories(i2pDataDir);
|
||||||
|
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
lastError = strprintf("Cannot create i2p data dir %s: %s", i2pDataDir.c_str(), e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
printf("Embedded I2P: starting i2pd router...\n");
|
printf("Embedded I2P: starting i2pd router...\n");
|
||||||
|
|
||||||
// Write an i2pd.conf configuration file that enables SAM + SOCKS proxy.
|
// Write an i2pd.conf configuration file that enables SAM + SOCKS proxy.
|
||||||
// i2pd's config system reads from a file; programmatic option setting is
|
// NOTE: As of i2pd 2.60.0, the embedded library API (i2p::api::InitI2P)
|
||||||
// fragile across i2pd versions. Writing a minimal conf is robust.
|
// never calls ParseConfig, so this file is NOT read at runtime. It is
|
||||||
|
// written for documentation/debugging purposes only — operators can
|
||||||
|
// inspect it to see what ports the daemon intends to use. The actual
|
||||||
|
// port bindings are applied programmatically via i2p::config::SetOption
|
||||||
|
// below (before the background thread starts). Keep the file in sync
|
||||||
|
// with the SetOption calls.
|
||||||
{
|
{
|
||||||
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
|
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
|
||||||
std::ofstream conf(confPath.string());
|
std::ofstream conf(confPath.string());
|
||||||
@@ -425,6 +456,8 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
conf << "# Auto-generated by Triangles embedded I2P\n";
|
conf << "# Auto-generated by Triangles embedded I2P\n";
|
||||||
|
conf << "# NOTE: i2pd 2.60.0 library API does NOT read this file.\n";
|
||||||
|
conf << "# Actual port bindings come from i2p::config::SetOption in i2p_embedded.cpp.\n";
|
||||||
conf << "datadir = " << i2pDataDir << "\n";
|
conf << "datadir = " << i2pDataDir << "\n";
|
||||||
conf << "loglevel = info\n";
|
conf << "loglevel = info\n";
|
||||||
conf << "\n";
|
conf << "\n";
|
||||||
@@ -441,6 +474,13 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
conf << "address = 127.0.0.1\n";
|
conf << "address = 127.0.0.1\n";
|
||||||
conf << "port = " << samPort << "\n";
|
conf << "port = " << samPort << "\n";
|
||||||
conf << "\n";
|
conf << "\n";
|
||||||
|
// Disable HTTP proxy (port 4444). Cycle-13 fix: the HTTPProxy runs by
|
||||||
|
// default in i2pd 2.60.0 and any HTTP request to its port causes a
|
||||||
|
// nullptr deref in i2p::i18n::Locale::GetString. Conf is dead code in
|
||||||
|
// the embedded library path; SetOption in InitI2P is the real override.
|
||||||
|
conf << "[httpproxy]\n";
|
||||||
|
conf << "enabled = false\n";
|
||||||
|
conf << "\n";
|
||||||
// Disable HTTP webconsole (not needed for embedded use)
|
// Disable HTTP webconsole (not needed for embedded use)
|
||||||
conf << "[http]\n";
|
conf << "[http]\n";
|
||||||
conf << "enabled = false\n";
|
conf << "enabled = false\n";
|
||||||
@@ -463,20 +503,29 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
if (serverPort > 0) {
|
if (serverPort > 0) {
|
||||||
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
|
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
|
||||||
std::ofstream tunnelConf(tunnelConfPath.string());
|
std::ofstream tunnelConf(tunnelConfPath.string());
|
||||||
if (tunnelConf.is_open()) {
|
if (!tunnelConf.is_open()) {
|
||||||
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
|
lastError = strprintf("Failed to write %s for server tunnel configuration",
|
||||||
tunnelConf << "[triangles-p2p]\n";
|
tunnelConfPath.string().c_str());
|
||||||
tunnelConf << "type = server\n";
|
printf("ERROR: %s\n", lastError.c_str());
|
||||||
tunnelConf << "host = 127.0.0.1\n";
|
return false;
|
||||||
tunnelConf << "port = " << serverPort << "\n";
|
|
||||||
tunnelConf << "keys = triangles-p2p-keys.dat\n";
|
|
||||||
tunnelConf << "inbound.length = 3\n";
|
|
||||||
tunnelConf << "outbound.length = 3\n";
|
|
||||||
tunnelConf << "inbound.quantity = 5\n";
|
|
||||||
tunnelConf << "outbound.quantity = 5\n";
|
|
||||||
tunnelConf.close();
|
|
||||||
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
|
|
||||||
}
|
}
|
||||||
|
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
|
||||||
|
tunnelConf << "[triangles-p2p]\n";
|
||||||
|
tunnelConf << "type = server\n";
|
||||||
|
tunnelConf << "host = 127.0.0.1\n";
|
||||||
|
tunnelConf << "port = " << serverPort << "\n";
|
||||||
|
tunnelConf << "keys = triangles-p2p-keys.dat\n";
|
||||||
|
tunnelConf << "inbound.length = 3\n";
|
||||||
|
tunnelConf << "outbound.length = 3\n";
|
||||||
|
tunnelConf << "inbound.quantity = 5\n";
|
||||||
|
tunnelConf << "outbound.quantity = 5\n";
|
||||||
|
tunnelConf.close();
|
||||||
|
if (tunnelConf.fail()) {
|
||||||
|
lastError = strprintf("Write failed for %s", tunnelConfPath.string().c_str());
|
||||||
|
printf("ERROR: %s\n", lastError.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build argv for i2pd initialization. Pass --datadir and --conf on the
|
// Build argv for i2pd initialization. Pass --datadir and --conf on the
|
||||||
@@ -494,14 +543,36 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
argvPtrs.push_back(&s[0]);
|
argvPtrs.push_back(&s[0]);
|
||||||
argvPtrs.push_back(nullptr);
|
argvPtrs.push_back(nullptr);
|
||||||
|
|
||||||
|
// The whole post-InitI2P section is wrapped in try/catch so that ANY
|
||||||
|
// failure after i2pd is initialized triggers TerminateI2P. Without this
|
||||||
|
// an exception from SetOption or std::thread construction would leave
|
||||||
|
// running=true but with no router thread to clean up — a leaked i2pd.
|
||||||
try {
|
try {
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Phase 1 (synchronous, < 1s): config parse, crypto, router context
|
// Phase 1 (synchronous, < 1s): config parse, crypto, router context.
|
||||||
|
// InitI2P is wrapped in try/catch so a partial-init failure does
|
||||||
|
// not leave i2pd in a half-initialized state with running=true.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
try {
|
||||||
|
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
lastError = strprintf("InitI2P failed: %s", e.what());
|
||||||
|
// Best-effort cleanup: i2pd's InitI2P may have partially
|
||||||
|
// initialized global state. TerminateI2P is a no-op if no
|
||||||
|
// init happened; it cleans up otherwise.
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
|
return false;
|
||||||
|
} catch (...) {
|
||||||
|
lastError = "InitI2P failed: unknown exception";
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
|
||||||
// Mark running immediately so Qt UI shows I2P as active.
|
// Mark running immediately so Qt UI shows I2P as active.
|
||||||
|
// From this point on, any exception thrown by the code below is
|
||||||
|
// caught by the outer try/catch, which calls TerminateI2P to
|
||||||
|
// release the partially-initialized i2pd state.
|
||||||
running.store(true);
|
running.store(true);
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -524,6 +595,64 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
printf("Embedded I2P: launching router in background thread...\n");
|
printf("Embedded I2P: launching router in background thread...\n");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// PROGRAMMATIC OVERRIDE OF SOCKS/SAM PORTS
|
||||||
|
//
|
||||||
|
// i2pd 2.60.0's library API (i2p::api::InitI2P) only calls ParseCmdline
|
||||||
|
// — it never calls ParseConfig. The i2pd.conf file we just wrote is
|
||||||
|
// NEVER READ by the embedded library path. SOCKS proxy falls back to
|
||||||
|
// its built-in default port (4447) regardless of what we put in the
|
||||||
|
// conf file. This is verified by the library's bundled ParseConfig
|
||||||
|
// (called only by the standalone daemon binary at
|
||||||
|
// src/i2p/i2pd-src/daemon/Daemon.cpp:107) — the library API
|
||||||
|
// deliberately omits it.
|
||||||
|
//
|
||||||
|
// The fix: override socksproxy.port + sam.port + socksproxy.address
|
||||||
|
// AFTER i2p::api::InitI2P returns (so all defaults are in m_Options)
|
||||||
|
// but BEFORE the background thread calls i2p::client::context.Start
|
||||||
|
// which calls ReadSocksProxy + ReadSAMBridge. SetOption calls
|
||||||
|
// notify() internally, so the new values are visible to GetOption.
|
||||||
|
//
|
||||||
|
// NOTE: SOCKS/SAM port range validation happens in Start() Phase 0
|
||||||
|
// before any state mutation, so by this point socksPort and samPort
|
||||||
|
// are already known to be 1..65535. No re-validation needed here.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
printf("Embedded I2P: overriding socksproxy.port=%d sam.port=%d via SetOption\n",
|
||||||
|
socksPort, samPort);
|
||||||
|
fflush(stdout);
|
||||||
|
{
|
||||||
|
bool socksEnabled = true;
|
||||||
|
std::string socksAddr = "127.0.0.1";
|
||||||
|
uint16_t socksPortVal = (uint16_t)socksPort;
|
||||||
|
std::string socksKeys = "socks-proxy.dat";
|
||||||
|
bool samEnabled = true;
|
||||||
|
std::string samAddr = "127.0.0.1";
|
||||||
|
uint16_t samPortVal = (uint16_t)samPort;
|
||||||
|
// Cycle-13 fix: HTTPProxy runs by default in i2pd 2.60.0 on
|
||||||
|
// port 4444 and any inbound HTTP request crashes the daemon via
|
||||||
|
// nullptr deref in i2p::i18n::Locale::GetString (m_Language is
|
||||||
|
// never initialized). The previous SetOption("http.enabled",...)
|
||||||
|
// targeted the i2pd WEBCONSOLE, not the HTTPProxy. Correct key
|
||||||
|
// is "httpproxy.enabled".
|
||||||
|
bool httpproxyEnabled = false;
|
||||||
|
bool httpWebconsoleEnabled = false;
|
||||||
|
bool i2pcontrolEnabled = false;
|
||||||
|
bool bobEnabled = false;
|
||||||
|
|
||||||
|
i2p::config::SetOption("socksproxy.enabled", socksEnabled);
|
||||||
|
i2p::config::SetOption("socksproxy.address", socksAddr);
|
||||||
|
i2p::config::SetOption("socksproxy.port", socksPortVal);
|
||||||
|
i2p::config::SetOption("socksproxy.keys", socksKeys);
|
||||||
|
i2p::config::SetOption("sam.enabled", samEnabled);
|
||||||
|
i2p::config::SetOption("sam.address", samAddr);
|
||||||
|
i2p::config::SetOption("sam.port", samPortVal);
|
||||||
|
// Cycle-13 fix: was "http.enabled" which targeted webconsole.
|
||||||
|
i2p::config::SetOption("httpproxy.enabled", httpproxyEnabled);
|
||||||
|
i2p::config::SetOption("http.enabled", httpWebconsoleEnabled);
|
||||||
|
i2p::config::SetOption("i2pcontrol.enabled", i2pcontrolEnabled);
|
||||||
|
i2p::config::SetOption("bob.enabled", bobEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the thread handle so Stop() can join it. A detached thread
|
// Keep the thread handle so Stop() can join it. A detached thread
|
||||||
// that is still running would block the wallet from exiting.
|
// that is still running would block the wallet from exiting.
|
||||||
routerThread = std::thread([this]() {
|
routerThread = std::thread([this]() {
|
||||||
@@ -603,19 +732,35 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate .b32.i2p address
|
// ----------------------------------------------------------------
|
||||||
try {
|
// Populate .b32.i2p address — use the SERVER TUNNEL destination,
|
||||||
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
|
// NOT the embedded router identity. Delegates to
|
||||||
i2pHostname = identHash.ToBase32() + ".b32.i2p";
|
// DiscoverServerTunnelDestination() which is also callable
|
||||||
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
|
// from GetI2PAddress() so the Qt timerI2P retry path picks
|
||||||
} catch (...) {
|
// up the result once the tunnel registers.
|
||||||
printf("Embedded I2P: .b32.i2p address not yet available, Qt timer will retry\n");
|
// ----------------------------------------------------------------
|
||||||
}
|
DiscoverServerTunnelDestination();
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
// Background init failure: i2pd router context may be partially
|
||||||
|
// alive (transports listening, netDb half-built). Tear it down,
|
||||||
|
// reset running, and surface the error in lastError so callers
|
||||||
|
// can see the failure rather than seeing running=true forever.
|
||||||
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
|
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
lastError = std::string("i2pd background init failed: ") + e.what();
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
|
running.store(false);
|
||||||
|
} catch (...) {
|
||||||
|
// Catch-all: any non-std::exception (e.g. structured exception
|
||||||
|
// on Windows) would otherwise invoke std::terminate, killing
|
||||||
|
// the daemon with no useful diagnostic.
|
||||||
|
printf("ERROR: Embedded I2P background init failed: unknown exception\n");
|
||||||
|
fflush(stdout);
|
||||||
|
lastError = "i2pd background init failed: unknown exception";
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
|
running.store(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -627,6 +772,16 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
|||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
lastError = std::string("i2pd initialization failed: ") + e.what();
|
lastError = std::string("i2pd initialization failed: ") + e.what();
|
||||||
printf("ERROR: Embedded I2P startup failed: %s\n", e.what());
|
printf("ERROR: Embedded I2P startup failed: %s\n", e.what());
|
||||||
|
// i2pd may be partially or fully initialized by the time we got here.
|
||||||
|
// TerminateI2P is a no-op if InitI2P never ran; otherwise it cleans
|
||||||
|
// up router context, transports, and netDb.
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
|
running.store(false);
|
||||||
|
return false;
|
||||||
|
} catch (...) {
|
||||||
|
lastError = "i2pd initialization failed: unknown exception";
|
||||||
|
printf("ERROR: Embedded I2P startup failed: unknown exception\n");
|
||||||
|
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||||
running.store(false);
|
running.store(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -674,6 +829,160 @@ void CI2PEmbedded::Stop()
|
|||||||
running.store(false);
|
running.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// CI2PEmbedded::DiscoverServerTunnelDestination
|
||||||
|
//
|
||||||
|
// Reads triangles-p2p-keys.dat (binary PrivateKeys blob) and looks for
|
||||||
|
// a matching entry in i2p::client::context.GetServerTunnels(). On
|
||||||
|
// match, sets i2pHostname to the corresponding ".b32.i2p" address.
|
||||||
|
// On no match (or serverPort==0, or read failure), leaves i2pHostname
|
||||||
|
// empty. Idempotent and safe to call repeatedly from the Qt timerI2P
|
||||||
|
// path (qt/trianglesgui.cpp:384-387, default 5s interval).
|
||||||
|
//
|
||||||
|
// Why fail-closed: publishing the keys-file hash while the tunnel is
|
||||||
|
// not yet registered would mean advertising a destination with no
|
||||||
|
// published LeaseSet → peers hit SOCKS code 4 / "LeaseSet not found"
|
||||||
|
// on the floodfill network. Empty hostname → no wrong-identity
|
||||||
|
// connectivity. The Qt timer keeps retrying until the tunnel comes up.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
void CI2PEmbedded::DiscoverServerTunnelDestination()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||||
|
|
||||||
|
if (serverPort == 0) {
|
||||||
|
// No P2P server tunnel configured. This is the -nolisten / no
|
||||||
|
// -i2phsport case (pure outbound SOCKS I2P, no inbound service).
|
||||||
|
if (!i2pHostname.empty()) {
|
||||||
|
printf("Embedded I2P: serverPort=0, clearing previously "
|
||||||
|
"discovered destination\n");
|
||||||
|
i2pHostname.clear();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool advertised = false;
|
||||||
|
std::string serverKeysPath = (fs::path(i2pDataDir) / "triangles-p2p-keys.dat").string();
|
||||||
|
|
||||||
|
// Step 1: read expected ident hash from the keys file.
|
||||||
|
std::string keysFileIdentB32;
|
||||||
|
try {
|
||||||
|
std::ifstream ks(serverKeysPath, std::ifstream::binary);
|
||||||
|
if (ks.is_open()) {
|
||||||
|
ks.seekg(0, std::ios::end);
|
||||||
|
size_t len = ks.tellg();
|
||||||
|
ks.seekg(0, std::ios::beg);
|
||||||
|
if (len == 0 || len > 65536) {
|
||||||
|
throw std::runtime_error("implausible keys file size: " +
|
||||||
|
std::to_string(len));
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> buf(len);
|
||||||
|
ks.read(reinterpret_cast<char*>(buf.data()), len);
|
||||||
|
if (!ks) {
|
||||||
|
throw std::runtime_error("short read on keys file");
|
||||||
|
}
|
||||||
|
i2p::data::PrivateKeys pk;
|
||||||
|
if (!pk.FromBuffer(buf.data(), len)) {
|
||||||
|
throw std::runtime_error("PrivateKeys::FromBuffer failed");
|
||||||
|
}
|
||||||
|
auto pub = pk.GetPublic();
|
||||||
|
if (!pub) {
|
||||||
|
throw std::runtime_error("PrivateKeys::GetPublic returned null");
|
||||||
|
}
|
||||||
|
keysFileIdentB32 = pub->GetIdentHash().ToBase32();
|
||||||
|
} else {
|
||||||
|
// Quiet on retry — file-not-found is expected before the
|
||||||
|
// bootstrap thread writes it for the first time.
|
||||||
|
if (i2pHostname.empty()) {
|
||||||
|
// First failure: log at info level so the operator can
|
||||||
|
// see why the hostname is still empty.
|
||||||
|
printf("Embedded I2P: cannot open %s for server tunnel keys "
|
||||||
|
"(will retry on next timerI2P tick)\n",
|
||||||
|
serverKeysPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
lastDiscoveryError = e.what();
|
||||||
|
printf("Embedded I2P: keys-file ident hash load failed: %s\n", e.what());
|
||||||
|
} catch (...) {
|
||||||
|
lastDiscoveryError = "unknown exception";
|
||||||
|
printf("Embedded I2P: keys-file ident hash load failed: unknown exception\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: only publish if a LIVE registered server tunnel matches
|
||||||
|
// the keys-file hash. Registry membership confirms the tunnel is
|
||||||
|
// active; LeaseSet publication is i2pd's responsibility after that.
|
||||||
|
//
|
||||||
|
// Thread-safety: GetServerTunnels() returns a const reference to
|
||||||
|
// i2pd's internal m_ServerTunnels map, which has NO internal lock.
|
||||||
|
// VisitTunnels(true) (called from ReloadConfig / Stop) can erase
|
||||||
|
// entries concurrently. We make a VALUE COPY of the map (not a
|
||||||
|
// reference) so that iterator invalidation during the copy is a
|
||||||
|
// narrow read-only window, and all string comparisons run on the
|
||||||
|
// local snapshot with no live-map access. The copy constructor of
|
||||||
|
// std::map is exception-safe; if it throws (bad_alloc), the catch
|
||||||
|
// below handles it.
|
||||||
|
if (!keysFileIdentB32.empty()) {
|
||||||
|
try {
|
||||||
|
auto tunnels = i2p::client::context.GetServerTunnels(); // value copy
|
||||||
|
for (const auto& kv : tunnels) {
|
||||||
|
const auto& dest = kv.first.first;
|
||||||
|
if (dest.ToBase32() == keysFileIdentB32) {
|
||||||
|
if (i2pHostname != keysFileIdentB32 + ".b32.i2p") {
|
||||||
|
i2pHostname = keysFileIdentB32 + ".b32.i2p";
|
||||||
|
lastDiscoveryError.clear(); // success — clear stale
|
||||||
|
printf("Embedded I2P: server tunnel address "
|
||||||
|
"(live registry) = %s\n", i2pHostname.c_str());
|
||||||
|
}
|
||||||
|
advertised = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
lastDiscoveryError = e.what();
|
||||||
|
printf("Embedded I2P: server tunnel registry read failed: %s\n", e.what());
|
||||||
|
} catch (...) {
|
||||||
|
lastDiscoveryError = "unknown exception";
|
||||||
|
printf("Embedded I2P: server tunnel registry read failed: "
|
||||||
|
"unknown exception\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!advertised) {
|
||||||
|
// Not yet in live registry. Leave empty (or clear stale value).
|
||||||
|
if (!i2pHostname.empty()) {
|
||||||
|
printf("Embedded I2P: server tunnel left live registry, "
|
||||||
|
"clearing destination %s\n", i2pHostname.c_str());
|
||||||
|
i2pHostname.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CI2PEmbedded::GetI2PAddress — read the cached destination, retrying
|
||||||
|
// discovery if empty. Called from qt/trianglesgui.cpp:1875
|
||||||
|
// (updateI2PAddress) on every timerI2P tick.
|
||||||
|
//
|
||||||
|
// Threading: read-by-copy under hostnameMutex so concurrent writes by
|
||||||
|
// the bootstrap thread cannot tear the std::string.
|
||||||
|
std::string CI2PEmbedded::GetI2PAddress()
|
||||||
|
{
|
||||||
|
bool needDiscovery = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||||
|
needDiscovery = i2pHostname.empty() && running.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needDiscovery) {
|
||||||
|
// Tunnel may have registered since the bootstrap-thread scan.
|
||||||
|
// Re-scans the registry and the keys file (does file I/O); not
|
||||||
|
// "cheap" on retry, but bounded — single registry walk + one
|
||||||
|
// small file read.
|
||||||
|
DiscoverServerTunnelDestination();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||||
|
return i2pHostname;
|
||||||
|
}
|
||||||
|
|
||||||
#else // !ENABLE_I2P_EMBEDDED
|
#else // !ENABLE_I2P_EMBEDDED
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -696,6 +1005,14 @@ void CI2PEmbedded::Stop()
|
|||||||
running.store(false);
|
running.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stubs for the new methods (header declares them unconditionally)
|
||||||
|
void CI2PEmbedded::DiscoverServerTunnelDestination() {}
|
||||||
|
std::string CI2PEmbedded::GetI2PAddress()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||||
|
return i2pHostname;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // ENABLE_I2P_EMBEDDED
|
#endif // ENABLE_I2P_EMBEDDED
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
+37
-3
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
// Cross-platform socket handle for SAM v3 streaming API.
|
// Cross-platform socket handle for SAM v3 streaming API.
|
||||||
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
|
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
|
||||||
@@ -92,12 +94,41 @@ private:
|
|||||||
int samPort; // i2pd SAM bridge port (for SAM v3 protocol)
|
int samPort; // i2pd SAM bridge port (for SAM v3 protocol)
|
||||||
int serverPort; // Triangles P2P listen port (for incoming I2P connections)
|
int serverPort; // Triangles P2P listen port (for incoming I2P connections)
|
||||||
std::string i2pDataDir; // i2pd data directory (under wallet datadir)
|
std::string i2pDataDir; // i2pd data directory (under wallet datadir)
|
||||||
|
// Hostname and discovery-error cache are read by the Qt UI thread
|
||||||
|
// (qt/trianglesgui.cpp:1875 updateI2PAddress) on every 5s timerI2P
|
||||||
|
// tick and written by the bootstrap thread. Mutex-guarded to avoid
|
||||||
|
// a C++ data race on the std::string itself.
|
||||||
|
mutable std::mutex hostnameMutex;
|
||||||
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
|
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
|
||||||
std::string lastError;
|
std::string lastError;
|
||||||
// I2P bootstrap runs in a background thread; we keep the handle so Stop()
|
// I2P bootstrap runs in a background thread; we keep the handle so Stop()
|
||||||
// can join it. (A detached thread that is still running blocks process exit.)
|
// can join it. (A detached thread that is still running blocks process exit.)
|
||||||
std::thread routerThread;
|
std::thread routerThread;
|
||||||
|
|
||||||
|
// Server-tunnel destination discovery.
|
||||||
|
//
|
||||||
|
// Scans the live server tunnel registry (i2p::client::context
|
||||||
|
// ::GetServerTunnels()) for an entry whose ident hash matches the
|
||||||
|
// public key in triangles-p2p-keys.dat. Sets i2pHostname to the
|
||||||
|
// corresponding ".b32.i2p" address on success; leaves i2pHostname
|
||||||
|
// empty otherwise. Thread-safe: the registry scan is mutex-guarded
|
||||||
|
// inside libi2pd_client; we only read the resulting map.
|
||||||
|
//
|
||||||
|
// This is a no-op when serverPort == 0 (no inbound server tunnel
|
||||||
|
// configured — pure outbound SOCKS I2P mode).
|
||||||
|
//
|
||||||
|
// Idempotent. Called from the bootstrap thread AND from
|
||||||
|
// GetI2PAddress() when i2pHostname is empty, so the Qt timerI2P
|
||||||
|
// (qt/trianglesgui.cpp:384-387) picks up the result on its next
|
||||||
|
// 5s tick once the tunnel registers.
|
||||||
|
void DiscoverServerTunnelDestination();
|
||||||
|
|
||||||
|
// Cache the most recent discovery failure reason (parsed keys-file
|
||||||
|
// hash, registry-read error, etc.). Visible only to GetStartupError()
|
||||||
|
// callers in the header — no public accessor for lastDiscoveryError
|
||||||
|
// is needed today.
|
||||||
|
std::string lastDiscoveryError;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static CI2PEmbedded* GetInstance();
|
static CI2PEmbedded* GetInstance();
|
||||||
|
|
||||||
@@ -121,8 +152,11 @@ public:
|
|||||||
int GetServerPort() const { return serverPort; }
|
int GetServerPort() const { return serverPort; }
|
||||||
const std::string& GetDataDir() const { return i2pDataDir; }
|
const std::string& GetDataDir() const { return i2pDataDir; }
|
||||||
|
|
||||||
// Get our .b32.i2p destination address
|
// Get our .b32.i2p destination address. Triggers a discovery retry
|
||||||
std::string GetI2PAddress() const { return i2pHostname; }
|
// if the hostname is empty (e.g. first attempt raced the tunnel
|
||||||
|
// registration). Idempotent and cheap when the hostname is already
|
||||||
|
// populated.
|
||||||
|
std::string GetI2PAddress();
|
||||||
std::string GetStartupError() const { return lastError; }
|
std::string GetStartupError() const { return lastError; }
|
||||||
void SetStartupError(const std::string& value) { lastError = value; }
|
void SetStartupError(const std::string& value) { lastError = value; }
|
||||||
|
|
||||||
@@ -144,4 +178,4 @@ public:
|
|||||||
bool StartEmbeddedI2P();
|
bool StartEmbeddedI2P();
|
||||||
void StopEmbeddedI2P();
|
void StopEmbeddedI2P();
|
||||||
|
|
||||||
#endif // TRIANGLES_I2P_EMBEDDED_H
|
#endif // TRIANGLES_I2P_EMBEDDED_H
|
||||||
+10
-11
@@ -3,24 +3,23 @@
|
|||||||
|
|
||||||
// Hardcoded I2P seed nodes for initial peer discovery.
|
// Hardcoded I2P seed nodes for initial peer discovery.
|
||||||
// These are .b32.i2p addresses (Destination hashes).
|
// These are .b32.i2p addresses (Destination hashes).
|
||||||
// Nodes must run i2pd with a server tunnel forwarding to the Triangles P2P port.
|
// Nodes must run i2pd (embedded or external) with a server tunnel
|
||||||
|
// forwarding to the Triangles P2P port.
|
||||||
//
|
//
|
||||||
// NOTE: .b32.i2p addresses are derived from the destination's public key.
|
// NOTE: .b32.i2p addresses are derived from the destination's public key.
|
||||||
// They are generated when the node first creates its I2P tunnel keys.
|
// They are generated when the node first creates its I2P tunnel keys.
|
||||||
// Replace these placeholders with actual seed node addresses once deployed.
|
// These addresses were captured from running daemons via getnetworkinfo
|
||||||
|
// on 2026-08-05. See i2pseed-capture-2026-08-05.md for the raw outputs.
|
||||||
//
|
//
|
||||||
// Dynamic seeds will also be available at:
|
// Dynamic seeds are also available at:
|
||||||
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
|
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
|
||||||
static const char *strMainNetI2PSeed[][1] = {
|
static const char *strMainNetI2PSeed[][1] = {
|
||||||
// SAMI-PC - authoritative wallet node (main PC)
|
// SAMI-PC - authoritative wallet node (main PC). Captured 2026-08-05.
|
||||||
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
|
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
|
||||||
// DNS2 - primary bootstrap server (194.233.88.206)
|
// DNS2 - primary bootstrap server (194.233.88.206). Captured 2026-08-05.
|
||||||
// Generated by embedded i2pd on first run, keys persist in i2p_data/
|
{"7d5gujh6tw6xbd2uquedhpm3ixoglsgt3nkfqb4b5lvunhjdb2kq.b32.i2p"},
|
||||||
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
|
// DNS3 - canonical chain reference (74.208.167.19). Captured 2026-08-05.
|
||||||
// DNS3 - canonical chain reference (74.208.167.19)
|
{"jdrpj364rmdule7rw2jdl63wvk3kbaivuje7wyhayugjbxvgbj2a.b32.i2p"},
|
||||||
{"hvvr2yys3nll4l6fdywecvn3baw6h5i7bsa2ldbz2e5xwangnn7q.b32.i2p"},
|
|
||||||
// Hetzner Helsinki - ARM64 staking node (46.62.249.20)
|
|
||||||
{"2hyeunnkax5du4snip4gdsdicxtmlnagtlkatv57rjpx2kvfssma.b32.i2p"},
|
|
||||||
{nullptr}
|
{nullptr}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+116
-101
@@ -35,6 +35,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
// Forward declaration: InitError / InitWarning are defined further down
|
// Forward declaration: InitError / InitWarning are defined further down
|
||||||
// in this file but referenced by AppInit (line ~423) before the definition.
|
// in this file but referenced by AppInit (line ~423) before the definition.
|
||||||
@@ -45,6 +46,12 @@ static bool InitWarning(const std::string& str);
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <openssl/crypto.h>
|
#include <openssl/crypto.h>
|
||||||
|
|
||||||
|
#ifdef WIN32
|
||||||
|
// _get_osfhandle lives in <io.h>; FlushFileBuffers / HANDLE live in <windows.h>,
|
||||||
|
// which is transitively included via util.h on Windows builds.
|
||||||
|
#include <io.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
@@ -102,6 +109,49 @@ bool LockDataDirectory(const std::filesystem::path& pathLockFile)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SyncReindexMarker(const fs::path& markerPath)
|
||||||
|
{
|
||||||
|
// POSIX systems guarantee parent-directory durability via fsync(dirfd).
|
||||||
|
// Windows does not expose an equivalent primitive for directory metadata;
|
||||||
|
// `_commit` flushes the file's data to disk and the underlying NTFS
|
||||||
|
// journal commits the directory entry on close. Both paths below flush
|
||||||
|
// before close to maximise durability; Windows users get file-data
|
||||||
|
// durability equivalent to POSIX, with directory metadata committed by
|
||||||
|
// the journal.
|
||||||
|
FILE* marker = std::fopen(markerPath.string().c_str(), "wb");
|
||||||
|
if (!marker)
|
||||||
|
return false;
|
||||||
|
static const char text[] = "Reindex must complete successfully before normal startup.\n";
|
||||||
|
bool ok = std::fwrite(text, 1, sizeof(text) - 1, marker) == sizeof(text) - 1 &&
|
||||||
|
std::fflush(marker) == 0;
|
||||||
|
#ifdef WIN32
|
||||||
|
// FlushFileBuffers on the file handle commits data durably to NTFS.
|
||||||
|
intptr_t osHandle = _get_osfhandle(_fileno(marker));
|
||||||
|
if (osHandle == -1 || FlushFileBuffers(reinterpret_cast<HANDLE>(osHandle)) == FALSE)
|
||||||
|
ok = false;
|
||||||
|
#else
|
||||||
|
if (ok)
|
||||||
|
ok = ::fsync(fileno(marker)) == 0;
|
||||||
|
#endif
|
||||||
|
if (std::fclose(marker) != 0)
|
||||||
|
ok = false;
|
||||||
|
#ifdef WIN32
|
||||||
|
// No directory-fsync primitive on Windows. The journal commit on close
|
||||||
|
// (and the FlushFileBuffers above) is the strongest durability available.
|
||||||
|
// See comment block above.
|
||||||
|
#else
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
int dirFd = ::open(markerPath.parent_path().string().c_str(), O_RDONLY | O_DIRECTORY);
|
||||||
|
if (dirFd < 0)
|
||||||
|
return false;
|
||||||
|
ok = ::fsync(dirFd) == 0;
|
||||||
|
::close(dirFd);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error)
|
bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error)
|
||||||
{
|
{
|
||||||
@@ -178,95 +228,19 @@ void ExitTimeout(void* parg)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to maxWaitSec for at least minPeers peers to have reported their
|
// Automatic recovery is intentionally non-destructive. Older builds deleted the
|
||||||
// chain height via the version handshake. Returns the median peer height, or
|
// chain DB and blk0001.dat when a node lagged its peers, which could turn a
|
||||||
// -1 if we couldn't get enough peers (timeout, no peers, all nStartingHeight=-1).
|
// transient peer-height report into permanent local data loss. A privacy coin
|
||||||
int WaitForPeerHeights(int minPeers, int maxWaitSec)
|
// must never rewrite historical chain data automatically; recovery remains an
|
||||||
|
// explicit operator action after wallet and chain-state backups.
|
||||||
|
// Legacy hook retained only to surface that -autorerebuild no longer mutates
|
||||||
|
// chain state.
|
||||||
|
static void LogAutoRebuildDisabled(int thresholdBlocks)
|
||||||
{
|
{
|
||||||
const int pollIntervalMs = 500;
|
if (thresholdBlocks > 0) {
|
||||||
const int64_t deadline = GetTimeMillis() + (int64_t)maxWaitSec * 1000;
|
printf("AutoRebuild: -autorerebuild=%d ignored; automatic chain deletion is disabled.\n",
|
||||||
|
thresholdBlocks);
|
||||||
while (GetTimeMillis() < deadline && !fRequestShutdown) {
|
|
||||||
std::vector<int> heights;
|
|
||||||
{
|
|
||||||
LOCK(cs_vNodes);
|
|
||||||
for (CNode* pnode : vNodes) {
|
|
||||||
if (pnode && pnode->nStartingHeight > 0)
|
|
||||||
heights.push_back(pnode->nStartingHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((int)heights.size() >= minPeers) {
|
|
||||||
std::sort(heights.begin(), heights.end());
|
|
||||||
int median = heights[heights.size() / 2];
|
|
||||||
printf("AutoRebuild: got %zu peer heights; median=%d\n", heights.size(), median);
|
|
||||||
return median;
|
|
||||||
}
|
|
||||||
MilliSleep(pollIntervalMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<int> heights;
|
|
||||||
{
|
|
||||||
LOCK(cs_vNodes);
|
|
||||||
for (CNode* pnode : vNodes) {
|
|
||||||
if (pnode && pnode->nStartingHeight > 0)
|
|
||||||
heights.push_back(pnode->nStartingHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (heights.empty()) {
|
|
||||||
printf("AutoRebuild: no peers reported heights after %ds\n", maxWaitSec);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
std::sort(heights.begin(), heights.end());
|
|
||||||
int median = heights[heights.size() / 2];
|
|
||||||
printf("AutoRebuild: timed out with %zu peers; median=%d\n", heights.size(), median);
|
|
||||||
return median;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If -autorerebuild is set and our local chain is more than that many blocks
|
|
||||||
// behind the median peer height, wipe the chain DB (preserving wallet.dat +
|
|
||||||
// onion + smsg state) and request shutdown. On restart, the daemon sees no
|
|
||||||
// chain DB and the snapshot path takes over.
|
|
||||||
void MaybeAutoRebuild(int thresholdBlocks)
|
|
||||||
{
|
|
||||||
if (thresholdBlocks <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (nBestHeight < 0) {
|
|
||||||
printf("AutoRebuild: local nBestHeight unset — skipping\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("AutoRebuild: enabled (threshold=%d blocks). Local chain tip: %d\n",
|
|
||||||
thresholdBlocks, nBestHeight);
|
|
||||||
int medianPeer = WaitForPeerHeights(/*minPeers=*/3, /*maxWaitSec=*/60);
|
|
||||||
if (medianPeer <= 0) {
|
|
||||||
printf("AutoRebuild: could not get peer heights — skipping rebuild\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int lag = medianPeer - nBestHeight;
|
|
||||||
printf("AutoRebuild: peer median=%d, local=%d, lag=%d\n",
|
|
||||||
medianPeer, nBestHeight, lag);
|
|
||||||
|
|
||||||
if (lag < thresholdBlocks) {
|
|
||||||
printf("AutoRebuild: lag %d < threshold %d — no rebuild needed\n",
|
|
||||||
lag, thresholdBlocks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("\n*** AutoRebuild: chain is %d blocks behind — wiping chain DB ***\n", lag);
|
|
||||||
printf("*** Preserving wallet.dat, smsgDB, onion state. ***\n");
|
|
||||||
printf("*** Daemon will shutdown; restart to load signed UTXO snapshot. ***\n\n");
|
|
||||||
|
|
||||||
WipeChainDataDir();
|
|
||||||
|
|
||||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
|
||||||
if (fs::exists(blkPath)) {
|
|
||||||
fs::remove(blkPath);
|
|
||||||
printf("AutoRebuild: removed stale %s\n", blkPath.string().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
StartShutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartShutdown()
|
void StartShutdown()
|
||||||
@@ -655,7 +629,7 @@ std::string HelpMessage()
|
|||||||
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
|
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
|
||||||
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
|
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
|
||||||
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
|
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
|
||||||
" -autorerebuild=<n> " + _("If our chain is more than <n> blocks behind peers, wipe chain DB and shutdown for clean restart (default: 0=disabled)") + "\n" +
|
" -autorerebuild=<n> " + _("Deprecated compatibility option; automatic chain deletion is disabled") + "\n" +
|
||||||
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
|
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
|
||||||
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
|
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
|
||||||
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
|
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
|
||||||
@@ -707,7 +681,8 @@ std::string HelpMessage()
|
|||||||
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
|
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
|
||||||
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
|
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
|
||||||
" -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" +
|
" -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" +
|
||||||
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
|
" -reindex " + _("Rebuild the derived chain database from the existing blk0001.dat without modifying the raw block file") + "\n" +
|
||||||
|
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
|
||||||
|
|
||||||
"\n" + _("Block creation options:") + "\n" +
|
"\n" + _("Block creation options:") + "\n" +
|
||||||
" -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" +
|
" -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" +
|
||||||
@@ -1424,22 +1399,66 @@ bool AppInit2()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
|
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
|
||||||
// blk*.dat files. This recalculates money
|
// blk0001.dat file used by this storage format. This recalculates money
|
||||||
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
|
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
|
||||||
// WipeChainDataDir(), which resolves the directory per the configured
|
// WipeChainDataDir(), which resolves the directory per the configured
|
||||||
// -chaindb backend.
|
// -chaindb backend.
|
||||||
if (GetBoolArg("-reindex", false))
|
const bool fReindex = GetBoolArg("-reindex", false);
|
||||||
|
fs::path reindexMarker = GetDataDir() / "REINDEX_INCOMPLETE";
|
||||||
|
|
||||||
|
// Validate the immutable source before removing any derived state. A marker
|
||||||
|
// survives crashes/interruption so ordinary startup cannot trust a partial
|
||||||
|
// database left by an earlier recovery attempt.
|
||||||
|
if (fReindex)
|
||||||
{
|
{
|
||||||
|
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||||
|
if (!fs::exists(blkPath) || !fs::is_regular_file(blkPath))
|
||||||
|
return InitError(_("Reindex requested but blk0001.dat is missing or not a regular file"));
|
||||||
|
if (!SyncReindexMarker(reindexMarker))
|
||||||
|
return InitError(_("Cannot durably create REINDEX_INCOMPLETE marker in the data directory"));
|
||||||
|
|
||||||
printf("Reindex requested: removing chain database...\n");
|
printf("Reindex requested: removing chain database...\n");
|
||||||
uiInterface.InitMessage(_("Removing chain database for reindex..."));
|
uiInterface.InitMessage(_("Removing chain database for reindex..."));
|
||||||
WipeChainDataDir();
|
WipeChainDataDir();
|
||||||
|
if (fs::exists(GetChainDataDir()))
|
||||||
|
return InitError(_("Reindex could not remove the existing chain database"));
|
||||||
|
}
|
||||||
|
else if (fs::exists(reindexMarker))
|
||||||
|
{
|
||||||
|
return InitError(_("A previous reindex was interrupted. Restart with -reindex to rebuild derived chain state."));
|
||||||
}
|
}
|
||||||
|
|
||||||
uiInterface.InitMessage(_("Loading block index..."));
|
uiInterface.InitMessage(_("Loading block index..."));
|
||||||
printf("Loading block index...\n");
|
printf("Loading block index...\n");
|
||||||
nStart = GetTimeMillis();
|
nStart = GetTimeMillis();
|
||||||
if (!LoadBlockIndex())
|
// Normal startup loads the existing derived index. An explicit -reindex
|
||||||
|
// must NOT call LoadBlockIndex() first: on an empty database that routine
|
||||||
|
// creates and appends a new genesis record to blk0001.dat. Reindex instead
|
||||||
|
// rebuilds directly from the already-existing raw history, keeping the
|
||||||
|
// source block file byte-for-byte unchanged.
|
||||||
|
if (fReindex)
|
||||||
|
{
|
||||||
|
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||||
|
if (!fs::exists(blkPath))
|
||||||
|
return InitError(_("Reindex requested but blk0001.dat is missing"));
|
||||||
|
|
||||||
|
printf("Reindex: rebuilding chain database from existing %s (raw block file will not be modified)\n",
|
||||||
|
blkPath.string().c_str());
|
||||||
|
uiInterface.InitMessage(_("Reindexing blocks from blk0001.dat..."));
|
||||||
|
int64_t nReindexStart = GetTimeMillis();
|
||||||
|
if (!FastImportBlockFile())
|
||||||
|
return InitError(_("Reindex failed while rebuilding from blk0001.dat"));
|
||||||
|
StartupPerfLog("reindex_fast_import", GetTimeMillis() - nReindexStart,
|
||||||
|
strprintf("bestheight=%d indexsize=%" PRIszu,
|
||||||
|
nBestHeight, mapBlockIndex.size()));
|
||||||
|
std::error_code markerError;
|
||||||
|
if (!fs::remove(reindexMarker, markerError) || markerError)
|
||||||
|
return InitError(_("Reindex completed but REINDEX_INCOMPLETE marker could not be removed"));
|
||||||
|
}
|
||||||
|
else if (!LoadBlockIndex())
|
||||||
|
{
|
||||||
return InitError(_("Error loading blkindex.dat"));
|
return InitError(_("Error loading blkindex.dat"));
|
||||||
|
}
|
||||||
|
|
||||||
// pindexLastHardenedCheckpoint is initialized from the hardened checkpoint
|
// pindexLastHardenedCheckpoint is initialized from the hardened checkpoint
|
||||||
// map on startup, BEFORE the daemon opens any peer connections or
|
// map on startup, BEFORE the daemon opens any peer connections or
|
||||||
@@ -1581,16 +1600,12 @@ bool AppInit2()
|
|||||||
uiInterface.InitMessage(_("UTXO rebuild complete"));
|
uiInterface.InitMessage(_("UTXO rebuild complete"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
|
// Keep the legacy option parse for compatibility, but automatic recovery is
|
||||||
// and shutdown for clean restart.
|
// diagnostic-only and never removes chain data.
|
||||||
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
|
LogAutoRebuildDisabled(GetArg("-autorerebuild", 0));
|
||||||
if (fRequestShutdown) {
|
|
||||||
printf("AutoRebuild: shutdown requested before chain load complete\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block index loaded. With fast-import removed, the only supported sync path
|
// Block index loaded. Normal bootstrap uses the UTXO snapshot; explicit
|
||||||
// is the UTXO snapshot (auto-downloaded from bootstrap or placed manually in datadir).
|
// -reindex is the operator-only recovery path from local blk0001.dat.
|
||||||
|
|
||||||
// as LoadBlockIndex can take several minutes, it's possible the user
|
// as LoadBlockIndex can take several minutes, it's possible the user
|
||||||
// requested to kill triangles-qt during the last operation. If so, exit.
|
// requested to kill triangles-qt during the last operation. If so, exit.
|
||||||
|
|||||||
+410
-98
@@ -27,6 +27,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -118,12 +119,12 @@ void ThreadForkDetector(void*)
|
|||||||
printf("*** Possible fork or sync stall. Check peers: 'getpeerinfo' and chain: 'getblockhash %d' ***\n",
|
printf("*** Possible fork or sync stall. Check peers: 'getpeerinfo' and chain: 'getblockhash %d' ***\n",
|
||||||
nOurHeight);
|
nOurHeight);
|
||||||
|
|
||||||
// If severe lag persists, suggest auto-rebuild
|
// Severe lag is diagnostic only. Recovery must be explicitly
|
||||||
if (lag >= threshold * 3 && GetBoolArg("-autorerebuild", 0) > 0)
|
// initiated by an operator after backups; never request an
|
||||||
|
// automatic shutdown that could lead to chain-state deletion.
|
||||||
|
if (lag >= threshold * 3 && GetBoolArg("-autorerebuild", false))
|
||||||
{
|
{
|
||||||
printf("*** FORK DETECTOR: lag %d >= %d, triggering AutoRebuild ***\n",
|
printf("*** FORK DETECTOR: automatic rebuild is disabled; operator review required ***\n");
|
||||||
lag, threshold * 3);
|
|
||||||
StartShutdown();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2666,19 +2667,25 @@ bool static Reorganize(CTxDBBase& txdb, CBlockIndex* pindexNew)
|
|||||||
// the pointer is NULL. In that state we still know the *height* of
|
// the pointer is NULL. In that state we still know the *height* of
|
||||||
// the checkpoint from the compiled map directly — every node built
|
// the checkpoint from the compiled map directly — every node built
|
||||||
// from the same binary sees the same value — and we use it as the
|
// 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
|
// fail-closed floor.
|
||||||
// attempt below the compiled checkpoint height would silently slip
|
//
|
||||||
// through the guard.
|
// 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;
|
int nHardenedCheckpointHeight = -1;
|
||||||
if (pindexLastHardenedCheckpoint)
|
if (pindexLastHardenedCheckpoint)
|
||||||
nHardenedCheckpointHeight = pindexLastHardenedCheckpoint->nHeight;
|
nHardenedCheckpointHeight = pindexLastHardenedCheckpoint->nHeight;
|
||||||
else
|
else
|
||||||
nHardenedCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
|
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);
|
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);
|
pfork->nHeight, nHardenedCheckpointHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4145,88 +4152,213 @@ bool LoadExternalBlockFile(FILE* fileIn)
|
|||||||
|
|
||||||
bool FastImportBlockFile()
|
bool FastImportBlockFile()
|
||||||
{
|
{
|
||||||
// Fast block import: reads blk0001.dat and builds the block index
|
// Explicit recovery importer: read the single raw block file used by this
|
||||||
// directly without re-writing block data. LevelDB writes are batched
|
// storage format and reconstruct all derived chain state without writing
|
||||||
// every 200K blocks for speed. Only used for trusted bootstrap data
|
// to blk0001.dat. The caller gates this behind -reindex.
|
||||||
// (blocks below the hardcoded checkpoint).
|
|
||||||
|
|
||||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||||
if (!fs::exists(blkPath))
|
if (!fs::exists(blkPath))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// LoadBlockIndex normally initializes these before opening the database.
|
||||||
|
// Reindex bypasses its genesis-creation path, so initialize the same
|
||||||
|
// network-specific framing and consensus parameters here.
|
||||||
|
if (fTestNet)
|
||||||
|
{
|
||||||
|
pchMessageStart[0] = 0x6f;
|
||||||
|
pchMessageStart[1] = 0x3e;
|
||||||
|
pchMessageStart[2] = 0x04;
|
||||||
|
pchMessageStart[3] = 0x13;
|
||||||
|
bnProofOfStakeLimit = bnProofOfStakeLimitTestNet;
|
||||||
|
bnProofOfWorkLimit = bnProofOfWorkLimitTestNet;
|
||||||
|
nStakeMinAge = 10 * 60;
|
||||||
|
nStakeMaxAge = 30 * 60;
|
||||||
|
nModifierInterval = 60;
|
||||||
|
nCoinbaseMaturity = 10;
|
||||||
|
nTargetSpacing = 60;
|
||||||
|
}
|
||||||
|
|
||||||
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
|
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
|
||||||
int64_t nStart = GetTimeMillis();
|
int64_t nStart = GetTimeMillis();
|
||||||
|
|
||||||
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
|
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
|
||||||
if (!fileIn)
|
if (!fileIn)
|
||||||
return false;
|
return false;
|
||||||
|
std::unique_ptr<FILE, int(*)(FILE*)> fileGuard(fileIn, &fclose);
|
||||||
|
|
||||||
// Get file size for progress
|
// Get file size for progress
|
||||||
fseek(fileIn, 0, SEEK_END);
|
if (fseek(fileIn, 0, SEEK_END) != 0)
|
||||||
|
return error("FastImportBlockFile: cannot seek to end of blk0001.dat");
|
||||||
int64_t nFileSize = ftell(fileIn);
|
int64_t nFileSize = ftell(fileIn);
|
||||||
fseek(fileIn, 0, SEEK_SET);
|
if (nFileSize <= 0 || nFileSize > (int64_t)std::numeric_limits<unsigned int>::max() ||
|
||||||
|
fseek(fileIn, 0, SEEK_SET) != 0)
|
||||||
|
return error("FastImportBlockFile: blk0001.dat size is invalid or exceeds the 32-bit disk-position format");
|
||||||
|
|
||||||
int nLoaded = 0;
|
int nLoaded = 0;
|
||||||
int64_t nLastProgressReport = 0;
|
int nRootBlocks = 0;
|
||||||
|
int64_t nLastRecordEnd = 0;
|
||||||
|
const uint256 expectedGenesis = fTestNet ? hashGenesisBlockTestNet : hashGenesisBlockOfficial;
|
||||||
|
|
||||||
{
|
{
|
||||||
LOCK(cs_main);
|
LOCK(cs_main);
|
||||||
CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION);
|
|
||||||
|
|
||||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
auto txdb_holder = MakeChainDB("cr+"); CTxDBBase& txdb = *txdb_holder;
|
||||||
txdb.TxnBegin();
|
if (!txdb.TxnBegin())
|
||||||
|
return error("FastImportBlockFile: failed to begin database transaction");
|
||||||
|
|
||||||
unsigned int nPos = 0;
|
try
|
||||||
while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown)
|
|
||||||
{
|
{
|
||||||
// Find message start bytes (same scan as LoadExternalBlockFile)
|
// The entire import runs inside this try block. The catch below
|
||||||
unsigned char pchData[65536];
|
// guarantees the in-flight transaction is explicitly aborted on
|
||||||
do {
|
// any exception (allocation, database, validation, or otherwise)
|
||||||
fseek(blkdat, nPos, SEEK_SET);
|
// before propagating, so a partial commit cannot leak even if the
|
||||||
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
|
// inner error paths miss a TxnAbort. Each inner error path also
|
||||||
if (nRead <= 8)
|
// aborts explicitly for clarity.
|
||||||
{
|
|
||||||
nPos = (unsigned int)-1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart));
|
|
||||||
if (nFind)
|
|
||||||
{
|
|
||||||
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0)
|
|
||||||
{
|
|
||||||
nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
nPos += ((unsigned char*)nFind - pchData) + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1;
|
|
||||||
} while(!fRequestShutdown);
|
|
||||||
|
|
||||||
if (nPos == (unsigned int)-1)
|
unsigned int nPos = 0;
|
||||||
break;
|
while ((int64_t)nPos < nFileSize && !fRequestShutdown)
|
||||||
|
{
|
||||||
fseek(blkdat, nPos, SEEK_SET);
|
// Strict contiguous framing: every record must begin exactly at
|
||||||
unsigned int nSize;
|
// nPos with network magic + declared payload size. Do not scan
|
||||||
blkdat >> nSize;
|
// forward through garbage; recovery must prove the whole file.
|
||||||
|
if (nFileSize - nPos < (int64_t)(sizeof(pchMessageStart) + sizeof(uint32_t)))
|
||||||
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
|
||||||
{
|
{
|
||||||
nPos += 4 + nSize;
|
txdb.TxnAbort();
|
||||||
continue;
|
return error("FastImportBlockFile: truncated record header at file offset %u", nPos);
|
||||||
|
}
|
||||||
|
unsigned char recordMagic[sizeof(pchMessageStart)];
|
||||||
|
if (fseek(fileIn, nPos, SEEK_SET) != 0 ||
|
||||||
|
fread(recordMagic, 1, sizeof(recordMagic), fileIn) != sizeof(recordMagic) ||
|
||||||
|
memcmp(recordMagic, pchMessageStart, sizeof(recordMagic)) != 0)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: invalid record magic at file offset %u", nPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// nBlockPos = file position where the block data starts
|
uint32_t nSize = 0;
|
||||||
// (after 4-byte message start + 4-byte size)
|
if (fread(&nSize, sizeof(nSize), 1, fileIn) != 1)
|
||||||
unsigned int nBlockPos = nPos + 4;
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: cannot read block size at file offset %u", nPos);
|
||||||
|
}
|
||||||
|
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: invalid block size %u at file offset %u", nSize, nPos);
|
||||||
|
}
|
||||||
|
const int64_t payloadPos = (int64_t)nPos + sizeof(pchMessageStart) + sizeof(nSize);
|
||||||
|
if (payloadPos + nSize > nFileSize)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: truncated block record at file offset %u", nPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> payload(nSize);
|
||||||
|
if (fread(payload.data(), 1, nSize, fileIn) != nSize)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: short payload read at file offset %u", nPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned int nBlockPos = (unsigned int)payloadPos;
|
||||||
CBlock block;
|
CBlock block;
|
||||||
blkdat >> block;
|
try
|
||||||
|
{
|
||||||
|
CDataStream record(payload.data(), payload.data() + payload.size(),
|
||||||
|
SER_DISK, CLIENT_VERSION);
|
||||||
|
record >> block;
|
||||||
|
if (!record.empty())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: block payload has %" PRIszu " trailing bytes at file offset %u",
|
||||||
|
record.size(), nPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: malformed block payload at file offset %u: %s",
|
||||||
|
nPos, e.what());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: unknown deserialization failure at file offset %u",
|
||||||
|
nPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned int nRecordEnd = (unsigned int)(payloadPos + nSize);
|
||||||
|
|
||||||
|
// Reindex is optimized for trusted local history but must still
|
||||||
|
// apply every context-free block/transaction invariant before it
|
||||||
|
// can write derived state. Context-dependent chain validity is
|
||||||
|
// anchored below by exact genesis, parent continuity, cumulative
|
||||||
|
// trust selection, and all compiled hardened checkpoints.
|
||||||
|
//
|
||||||
|
// PoS block-signature verification follows the runtime rule:
|
||||||
|
// - blocks above the newest compiled checkpoint must be
|
||||||
|
// individually signed and chain-trust valid;
|
||||||
|
// - blocks at or below the newest compiled checkpoint are
|
||||||
|
// covered by the historical assume-valid fast path, which
|
||||||
|
// is the same rule the daemon uses at runtime. We must NOT
|
||||||
|
// apply the per-block signature check unconditionally,
|
||||||
|
// because that policy change was deliberately added in
|
||||||
|
// v6.x to prevent chain splits over the pre-checkpoint era.
|
||||||
|
if (!block.CheckBlock(true, true, false))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: block failed context-free validation at file offset %u",
|
||||||
|
nPos);
|
||||||
|
}
|
||||||
|
if (block.IsProofOfStake() && pindexBest->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
||||||
|
!block.CheckBlockSignature())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: post-checkpoint block signature failure at file offset %u",
|
||||||
|
nPos);
|
||||||
|
}
|
||||||
|
|
||||||
uint256 hash = block.GetHash();
|
uint256 hash = block.GetHash();
|
||||||
|
if (block.hashPrevBlock == 0)
|
||||||
|
{
|
||||||
|
// The expected network genesis is the very first record in the
|
||||||
|
// file (offset 0). The runtime rule is "blocks whose parent is
|
||||||
|
// zero are only the genesis", and any other record with a zero
|
||||||
|
// parent would corrupt the active chain, so reject anything
|
||||||
|
// that hashes to the genesis hash anywhere other than offset 0.
|
||||||
|
++nRootBlocks;
|
||||||
|
if (hash == expectedGenesis)
|
||||||
|
{
|
||||||
|
if (nPos != 0 || nRootBlocks != 1)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: unexpected or duplicate genesis block %s at file offset %u",
|
||||||
|
hash.ToString().c_str(), nPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nPos == 0)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: first record is not the expected genesis block %s",
|
||||||
|
hash.ToString().c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Stray root record (previous broken -reindex runs may have
|
||||||
|
// appended a fresh genesis record to blk0001.dat). Skip it:
|
||||||
|
// it has no parent, no chain trust, and would otherwise be
|
||||||
|
// a false duplicate of genesis. Advance strictly so the
|
||||||
|
// exact-file-consumed invariant still holds.
|
||||||
|
nPos = nRecordEnd;
|
||||||
|
nLastRecordEnd = nPos;
|
||||||
|
nLoaded++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mapBlockIndex.count(hash))
|
if (mapBlockIndex.count(hash))
|
||||||
{
|
{
|
||||||
nPos += 4 + nSize;
|
nPos = nRecordEnd;
|
||||||
|
nLastRecordEnd = nPos;
|
||||||
continue; // already indexed
|
continue; // already indexed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4242,6 +4374,31 @@ bool FastImportBlockFile()
|
|||||||
pindexNew->pprev = miPrev->second;
|
pindexNew->pprev = miPrev->second;
|
||||||
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
||||||
}
|
}
|
||||||
|
else if (block.hashPrevBlock != 0)
|
||||||
|
{
|
||||||
|
delete pindexNew;
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: parent %s missing before block %s",
|
||||||
|
block.hashPrevBlock.ToString().c_str(), hash.ToString().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Checkpoints::CheckHardened(pindexNew->nHeight, hash))
|
||||||
|
{
|
||||||
|
const int badHeight = pindexNew->nHeight;
|
||||||
|
delete pindexNew;
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: hardened checkpoint mismatch at height %d",
|
||||||
|
badHeight);
|
||||||
|
}
|
||||||
|
if (pindexNew->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
||||||
|
!block.CheckBlockSignature())
|
||||||
|
{
|
||||||
|
const int badHeight = pindexNew->nHeight;
|
||||||
|
delete pindexNew;
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: post-checkpoint block signature failure at height %d",
|
||||||
|
badHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Chain trust
|
// Chain trust
|
||||||
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
||||||
@@ -4249,32 +4406,41 @@ bool FastImportBlockFile()
|
|||||||
// Stake entropy bit
|
// Stake entropy bit
|
||||||
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
|
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
|
||||||
|
|
||||||
// Stake modifier (minimal for blocks far below checkpoint)
|
// Recompute the exact historical stake-modifier chain. Every
|
||||||
int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
// block must participate: using placeholder zero modifiers for
|
||||||
if (pindexNew->nHeight >= nCheckpointHeight - 1000)
|
// older blocks leaves mature wallet UTXOs unable to resolve the
|
||||||
|
// later modifier required by CheckStakeKernelHash(). Reindex is
|
||||||
|
// an explicit recovery operation, so correctness takes priority
|
||||||
|
// over the old shortcut's speed.
|
||||||
|
uint64_t nStakeModifier = 0;
|
||||||
|
bool fGeneratedStakeModifier = false;
|
||||||
|
if (!ComputeNextStakeModifier(pindexNew->pprev,
|
||||||
|
nStakeModifier,
|
||||||
|
fGeneratedStakeModifier))
|
||||||
{
|
{
|
||||||
uint64_t nStakeModifier = 0;
|
delete pindexNew;
|
||||||
bool fGeneratedStakeModifier = false;
|
txdb.TxnAbort();
|
||||||
ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier);
|
return error("FastImportBlockFile: failed to compute stake modifier for block %s",
|
||||||
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
hash.ToString().c_str());
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0);
|
|
||||||
}
|
}
|
||||||
|
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);
|
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
||||||
|
|
||||||
// PoS stake seen set
|
// PoS stake seen set
|
||||||
if (pindexNew->IsProofOfStake())
|
if (pindexNew->IsProofOfStake())
|
||||||
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
||||||
|
|
||||||
// Insert into mapBlockIndex
|
// pnext is rebuilt after best-chain selection. File order also
|
||||||
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
|
// contains side branches, so assigning it here would let the last
|
||||||
pindexNew->phashBlock = &mi->first;
|
// imported child hijack stake-modifier forward walks.
|
||||||
|
|
||||||
// Link pnext for previous block
|
|
||||||
if (pindexNew->pprev)
|
|
||||||
pindexNew->pprev->pnext = pindexNew;
|
|
||||||
|
|
||||||
// NOTE: tx-index, UTXO-set and money-supply application are
|
// NOTE: tx-index, UTXO-set and money-supply application are
|
||||||
// DEFERRED to a second pass over the active (best-trust) chain
|
// DEFERRED to a second pass over the active (best-trust) chain
|
||||||
@@ -4285,7 +4451,12 @@ bool FastImportBlockFile()
|
|||||||
// That was the root cause of UTXO-set / supply inflation on every
|
// That was the root cause of UTXO-set / supply inflation on every
|
||||||
// reindex. Here we only build the block index for all blocks so
|
// reindex. Here we only build the block index for all blocks so
|
||||||
// best-chain selection by trust still works.
|
// best-chain selection by trust still works.
|
||||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew)))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to write block index %s",
|
||||||
|
hash.ToString().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Update best chain
|
// Update best chain
|
||||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||||
@@ -4303,14 +4474,27 @@ bool FastImportBlockFile()
|
|||||||
pindexGenesisBlock = pindexNew;
|
pindexGenesisBlock = pindexNew;
|
||||||
|
|
||||||
nLoaded++;
|
nLoaded++;
|
||||||
nPos += 4 + nSize;
|
nPos = nRecordEnd;
|
||||||
|
nLastRecordEnd = nPos;
|
||||||
|
|
||||||
// Batch commit every 200K blocks for LevelDB efficiency
|
// Batch commit every 200K blocks for LevelDB efficiency
|
||||||
if (nLoaded % 200000 == 0)
|
if (nLoaded % 200000 == 0)
|
||||||
{
|
{
|
||||||
txdb.WriteHashBestChain(hashBestChain);
|
if (!txdb.WriteHashBestChain(hashBestChain))
|
||||||
txdb.TxnCommit();
|
{
|
||||||
txdb.TxnBegin();
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: index batch WriteHashBestChain failed after %d blocks", nLoaded);
|
||||||
|
}
|
||||||
|
if (!txdb.TxnCommit())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: index batch TxnCommit failed after %d blocks", nLoaded);
|
||||||
|
}
|
||||||
|
if (!txdb.TxnBegin())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: index batch TxnBegin failed after %d blocks", nLoaded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report progress every 5000 blocks to keep GUI responsive.
|
// Report progress every 5000 blocks to keep GUI responsive.
|
||||||
@@ -4324,6 +4508,42 @@ bool FastImportBlockFile()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fRequestShutdown)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: interrupted after %d blocks; reindex is incomplete", nLoaded);
|
||||||
|
}
|
||||||
|
if (nRootBlocks < 1 || nLastRecordEnd != nFileSize)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: block file was not consumed exactly (roots=%d end=%" PRId64 " size=%" PRId64 ")",
|
||||||
|
nRootBlocks, nLastRecordEnd, nFileSize);
|
||||||
|
}
|
||||||
|
if (!pindexBest || !pindexGenesisBlock ||
|
||||||
|
pindexGenesisBlock->GetBlockHash() != expectedGenesis)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: no complete active chain found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int requiredCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
|
||||||
|
CBlockIndex* requiredCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||||
|
if (requiredCheckpointHeight < 0 || !requiredCheckpoint ||
|
||||||
|
requiredCheckpoint->nHeight != requiredCheckpointHeight)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: selected chain does not reach the newest compiled checkpoint at height %d",
|
||||||
|
requiredCheckpointHeight);
|
||||||
|
}
|
||||||
|
CBlockIndex* checkpointAncestor = pindexBest;
|
||||||
|
while (checkpointAncestor && checkpointAncestor->nHeight > requiredCheckpointHeight)
|
||||||
|
checkpointAncestor = checkpointAncestor->pprev;
|
||||||
|
if (checkpointAncestor != requiredCheckpoint)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: newest compiled checkpoint is not on selected active chain");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Pass 2: apply tx-index, UTXO set and money supply along the
|
// ---- Pass 2: apply tx-index, UTXO set and money supply along the
|
||||||
// ACTIVE (best-trust) chain ONLY. The file-order pass above indexed
|
// ACTIVE (best-trust) chain ONLY. The file-order pass above indexed
|
||||||
// every block including orphaned side-chain blocks; replaying only
|
// every block including orphaned side-chain blocks; replaying only
|
||||||
@@ -4335,6 +4555,15 @@ bool FastImportBlockFile()
|
|||||||
for (CBlockIndex* p = pindexBest; p; p = p->pprev)
|
for (CBlockIndex* p = pindexBest; p; p = p->pprev)
|
||||||
vMain.push_back(p);
|
vMain.push_back(p);
|
||||||
std::reverse(vMain.begin(), vMain.end());
|
std::reverse(vMain.begin(), vMain.end());
|
||||||
|
|
||||||
|
// File order includes side branches. Build pnext exclusively from
|
||||||
|
// the selected best-trust chain so kernel-modifier forward walks
|
||||||
|
// cannot follow whichever side-chain child appeared last.
|
||||||
|
for (const auto& item : mapBlockIndex)
|
||||||
|
item.second->pnext = nullptr;
|
||||||
|
for (size_t i = 1; i < vMain.size(); ++i)
|
||||||
|
vMain[i - 1]->pnext = vMain[i];
|
||||||
|
|
||||||
printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size());
|
printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size());
|
||||||
uiInterface.InitMessage(_("Building UTXO set (main chain)..."));
|
uiInterface.InitMessage(_("Building UTXO set (main chain)..."));
|
||||||
|
|
||||||
@@ -4342,6 +4571,13 @@ bool FastImportBlockFile()
|
|||||||
int nApplied = 0;
|
int nApplied = 0;
|
||||||
for (CBlockIndex* pindex : vMain)
|
for (CBlockIndex* pindex : vMain)
|
||||||
{
|
{
|
||||||
|
if (fRequestShutdown)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: interrupted during active-chain replay at height %d",
|
||||||
|
pindex->nHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Genesis (height 0) is a hardcoded special block that is not
|
// Genesis (height 0) is a hardcoded special block that is not
|
||||||
// re-read from disk this way; it contributes nothing to supply
|
// re-read from disk this way; it contributes nothing to supply
|
||||||
// and the genesis-walk audit skips it identically. Carry the
|
// and the genesis-walk audit skips it identically. Carry the
|
||||||
@@ -4350,13 +4586,20 @@ bool FastImportBlockFile()
|
|||||||
{
|
{
|
||||||
pindex->nMint = 0;
|
pindex->nMint = 0;
|
||||||
pindex->nMoneySupply = nRunningSupply; // still 0 here
|
pindex->nMoneySupply = nRunningSupply; // still 0 here
|
||||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to write genesis index");
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CBlock blockMain;
|
CBlock blockMain;
|
||||||
if (!blockMain.ReadFromDisk(pindex))
|
if (!blockMain.ReadFromDisk(pindex))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight);
|
return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight);
|
||||||
|
}
|
||||||
|
|
||||||
int64_t nBlockValueIn = 0;
|
int64_t nBlockValueIn = 0;
|
||||||
int64_t nBlockValueOut = 0;
|
int64_t nBlockValueOut = 0;
|
||||||
@@ -4366,7 +4609,12 @@ bool FastImportBlockFile()
|
|||||||
{
|
{
|
||||||
uint256 hashTx = tx.GetHash();
|
uint256 hashTx = tx.GetHash();
|
||||||
CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2);
|
CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2);
|
||||||
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
|
if (!txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size())))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to write txindex %s",
|
||||||
|
hashTx.ToString().c_str());
|
||||||
|
}
|
||||||
nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||||
|
|
||||||
nBlockValueOut += tx.GetValueOut();
|
nBlockValueOut += tx.GetValueOut();
|
||||||
@@ -4375,9 +4623,20 @@ bool FastImportBlockFile()
|
|||||||
for (const CTxIn& txin : tx.vin)
|
for (const CTxIn& txin : tx.vin)
|
||||||
{
|
{
|
||||||
CUtxoEntry uprev;
|
CUtxoEntry uprev;
|
||||||
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
if (!txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
||||||
nBlockValueIn += uprev.nValue;
|
{
|
||||||
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: missing spent UTXO %s:%u at height %d",
|
||||||
|
txin.prevout.hash.ToString().c_str(), txin.prevout.n,
|
||||||
|
pindex->nHeight);
|
||||||
|
}
|
||||||
|
nBlockValueIn += uprev.nValue;
|
||||||
|
if (!txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to erase spent UTXO %s:%u",
|
||||||
|
txin.prevout.hash.ToString().c_str(), txin.prevout.n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||||
@@ -4391,16 +4650,40 @@ bool FastImportBlockFile()
|
|||||||
utxo.fCoinBase = tx.IsCoinBase();
|
utxo.fCoinBase = tx.IsCoinBase();
|
||||||
utxo.fCoinStake = tx.IsCoinStake();
|
utxo.fCoinStake = tx.IsCoinStake();
|
||||||
utxo.nTxTime = tx.nTime;
|
utxo.nTxTime = tx.nTime;
|
||||||
txdb.WriteUtxo(hashTx, k, utxo);
|
if (!txdb.WriteUtxo(hashTx, k, utxo))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to write UTXO %s:%u",
|
||||||
|
hashTx.ToString().c_str(), k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pindex->nMint = nBlockValueOut - nBlockValueIn;
|
pindex->nMint = nBlockValueOut - nBlockValueIn;
|
||||||
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
||||||
pindex->nMoneySupply = nRunningSupply;
|
pindex->nMoneySupply = nRunningSupply;
|
||||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to update active block index at height %d",
|
||||||
|
pindex->nHeight);
|
||||||
|
}
|
||||||
|
|
||||||
if (++nApplied % 200000 == 0) { txdb.TxnCommit(); txdb.TxnBegin(); }
|
if (++nApplied % 200000 == 0)
|
||||||
|
{
|
||||||
|
if (!txdb.TxnCommit())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: active-chain batch TxnCommit failed at height %d",
|
||||||
|
pindex->nHeight);
|
||||||
|
}
|
||||||
|
if (!txdb.TxnBegin())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: active-chain batch TxnBegin failed at height %d",
|
||||||
|
pindex->nHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (nApplied % 5000 == 0)
|
if (nApplied % 5000 == 0)
|
||||||
{
|
{
|
||||||
int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size()));
|
int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size()));
|
||||||
@@ -4411,14 +4694,43 @@ bool FastImportBlockFile()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Final commit
|
// Final commit
|
||||||
if (pindexBest)
|
if (fRequestShutdown)
|
||||||
{
|
{
|
||||||
txdb.WriteHashBestChain(hashBestChain);
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: interrupted before final commit");
|
||||||
// Write sync checkpoint
|
}
|
||||||
Checkpoints::WriteSyncCheckpoint(hashBestChain);
|
if (!txdb.WriteHashBestChain(hashBestChain))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to persist best-chain hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sync checkpoint
|
||||||
|
if (!Checkpoints::WriteSyncCheckpoint(hashBestChain))
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: failed to persist sync checkpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!txdb.TxnCommit())
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: final database commit failed");
|
||||||
|
}
|
||||||
|
} // end try { ... FastImportBlockFile inner LOCK }
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
// Any exception escaping the import (allocation failure, database
|
||||||
|
// throw, unexpected validation throw) MUST NOT leak a partial
|
||||||
|
// commit. Abort the in-flight transaction before propagating.
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: uncaught exception during import: %s", e.what());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
txdb.TxnAbort();
|
||||||
|
return error("FastImportBlockFile: unknown exception during import");
|
||||||
}
|
}
|
||||||
txdb.TxnCommit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nTransactionsUpdated++;
|
nTransactionsUpdated++;
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ CBlockIndex* FindBlockByHeight(int nHeight);
|
|||||||
bool ProcessMessages(CNode* pfrom);
|
bool ProcessMessages(CNode* pfrom);
|
||||||
bool SendMessages(CNode* pto, bool fSendTrickle);
|
bool SendMessages(CNode* pto, bool fSendTrickle);
|
||||||
bool LoadExternalBlockFile(FILE* fileIn);
|
bool LoadExternalBlockFile(FILE* fileIn);
|
||||||
|
bool FastImportBlockFile();
|
||||||
|
|
||||||
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
|
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
|
||||||
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
|
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
|
||||||
|
|||||||
+248
-55
@@ -1,6 +1,9 @@
|
|||||||
#include "introdialog.h"
|
#include "introdialog.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "bootstrap.h"
|
#include "bootstrap.h"
|
||||||
|
#include "utxosnapshot.h"
|
||||||
|
#include "checkpoints.h"
|
||||||
|
#include "snapshotnet.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -14,9 +17,27 @@
|
|||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include <set>
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// Convert QString to fs::path preserving non-ASCII characters on Windows.
|
||||||
|
// On Windows, QString::toStdString() returns UTF-8 but std::filesystem::path
|
||||||
|
// constructed from a narrow string then uses the ANSI code page, which
|
||||||
|
// mangles UTF-8 paths. QString::toStdWString() + fs::path(std::wstring)
|
||||||
|
// preserves them. On non-Windows platforms the UTF-8 path is correct.
|
||||||
|
static fs::path qstringToPath(const QString& s)
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
return fs::path(std::wstring(s.toStdWString()));
|
||||||
|
#else
|
||||||
|
return fs::path(s.toStdString());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
IntroDialog::IntroDialog(QWidget *parent) :
|
IntroDialog::IntroDialog(QWidget *parent) :
|
||||||
QDialog(parent)
|
QDialog(parent)
|
||||||
@@ -152,7 +173,7 @@ void IntroDialog::on_defaultRadio_toggled(bool checked)
|
|||||||
void IntroDialog::updateFreeSpace()
|
void IntroDialog::updateFreeSpace()
|
||||||
{
|
{
|
||||||
QString path = getDataDirectory();
|
QString path = getDataDirectory();
|
||||||
std::filesystem::path fsPath(path.toStdString());
|
std::filesystem::path fsPath = qstringToPath(path);
|
||||||
|
|
||||||
// Walk up to find an existing parent
|
// Walk up to find an existing parent
|
||||||
try {
|
try {
|
||||||
@@ -211,14 +232,18 @@ bool IntroDialog::pickDataDirectory()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||||
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
QString defaultDir = QString::fromStdString(
|
||||||
|
std::string(GetDefaultDataDir().u8string()));
|
||||||
if (dataDir != defaultDir) {
|
if (dataDir != defaultDir) {
|
||||||
mapArgs["-datadir"] = dataDir.toStdString();
|
// Pass the data dir to the daemon as UTF-8 bytes so a non-ASCII path
|
||||||
|
// on Windows isn't mangled by the ANSI code page (path::string() does
|
||||||
|
// that). The daemon side uses fs::u8path() to convert back.
|
||||||
|
mapArgs["-datadir"] = std::string(qstringToPath(dataDir).u8string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
try {
|
try {
|
||||||
fs::create_directories(fs::path(dataDir.toStdString()));
|
fs::create_directories(qstringToPath(dataDir));
|
||||||
} catch (const fs::filesystem_error &) {
|
} catch (const fs::filesystem_error &) {
|
||||||
QMessageBox::critical(0, "Triangles",
|
QMessageBox::critical(0, "Triangles",
|
||||||
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
|
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
|
||||||
@@ -227,17 +252,24 @@ bool IntroDialog::pickDataDirectory()
|
|||||||
|
|
||||||
// Auto-bootstrap: if no blockchain data exists, download automatically.
|
// Auto-bootstrap: if no blockchain data exists, download automatically.
|
||||||
// If data exists, offer optional re-download (unless user checked "don't ask again").
|
// 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 needsBootstrap = Bootstrap::NeedsBootstrap(dataDirPath);
|
||||||
bool userWantsBootstrap = false;
|
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)
|
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",
|
QMessageBox::information(0, "Triangles",
|
||||||
"No blockchain data found.\n\n"
|
"No blockchain data found.\n\n"
|
||||||
"Downloading the latest blockchain snapshot automatically.\n"
|
"Setting up the wallet now — this only happens once.\n"
|
||||||
"This will only take a few minutes.");
|
"If a local snapshot is available it will be loaded automatically.");
|
||||||
userWantsBootstrap = true;
|
userWantsBootstrap = true;
|
||||||
}
|
}
|
||||||
else if (!settings.value("bootstrapDontAsk", false).toBool())
|
else if (!settings.value("bootstrapDontAsk", false).toBool())
|
||||||
@@ -263,59 +295,220 @@ bool IntroDialog::pickDataDirectory()
|
|||||||
userWantsBootstrap = (ret == QMessageBox::Yes);
|
userWantsBootstrap = (ret == QMessageBox::Yes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userWantsBootstrap)
|
if (userWantsBootstrap)
|
||||||
{
|
{
|
||||||
std::string host = Bootstrap::DEFAULT_HOST;
|
std::string host = Bootstrap::DEFAULT_HOST;
|
||||||
std::string strError;
|
std::string strError;
|
||||||
|
|
||||||
QProgressDialog progress("Downloading blockchain snapshot...", "Cancel",
|
// First-run local-snapshot probe: if utxo-snapshot.bin is already in the
|
||||||
0, 100, 0);
|
// data directory (placed there by the user, an installer, or a
|
||||||
progress.setWindowTitle("Triangles - Bootstrap");
|
// previous P2P fetch), load it directly. This avoids the HTTPS
|
||||||
progress.setWindowModality(Qt::ApplicationModal);
|
// bootstrap path entirely on first run.
|
||||||
progress.setMinimumDuration(0);
|
fs::path stagedSnap = dataDirPath / "utxo-snapshot.bin";
|
||||||
progress.setValue(0);
|
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) {
|
// Validate the staged file against the compiled-in hash before
|
||||||
if (totalBytes > 0) {
|
// touching the chain DB. This prevents loading a stale or wrong
|
||||||
int pct = (int)((bytesDownloaded * 100) / totalBytes);
|
// snapshot from a previous install into a fresh data dir.
|
||||||
progress.setValue(pct);
|
int snapHeight = Checkpoints::GetBestSnapshotHeight();
|
||||||
progress.setLabelText(
|
uint256 expectedHash;
|
||||||
QString("Downloading blockchain snapshot... %1 MB / %2 MB")
|
bool hashOk = (snapHeight > 0) &&
|
||||||
.arg(bytesDownloaded / (1024*1024))
|
Checkpoints::GetSnapshotHash(snapHeight, expectedHash);
|
||||||
.arg(totalBytes / (1024*1024)));
|
|
||||||
|
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 {
|
} else {
|
||||||
progress.setLabelText(
|
localErr = "cannot hash staged snapshot: " + hashErr;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
} 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",
|
QMessageBox::warning(0, "Triangles",
|
||||||
QString("Could not download blockchain snapshot:\n%1\n\n"
|
QString("Could not download blockchain snapshot:\n%1\n\n"
|
||||||
"The wallet will sync from the network instead.")
|
"The wallet will sync from the network instead.%2")
|
||||||
.arg(QString::fromStdString(strError)));
|
.arg(QString::fromStdString(strError))
|
||||||
} else {
|
.arg(localNote));
|
||||||
progress.setValue(100);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
progress.setValue(100);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -339,8 +532,8 @@ bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& ne
|
|||||||
{
|
{
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
fs::path srcDir(oldPath.toStdString());
|
fs::path srcDir = qstringToPath(oldPath);
|
||||||
fs::path dstDir(newPath.toStdString());
|
fs::path dstDir = qstringToPath(newPath);
|
||||||
|
|
||||||
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
|
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+4
-4
@@ -143,13 +143,13 @@ Value addnode(const Array& params, bool fHelp)
|
|||||||
"addnode <node> <add|remove|onetry>\n"
|
"addnode <node> <add|remove|onetry>\n"
|
||||||
"Attempts to add or remove a node from the addnode list,\n"
|
"Attempts to add or remove a node from the addnode list,\n"
|
||||||
"or try a connection to a node once.\n"
|
"or try a connection to a node once.\n"
|
||||||
"<node> must be a .onion address (Tor-native network).");
|
"<node> must be a .onion or .b32.i2p address (Tor+I2P dual-network).");
|
||||||
|
|
||||||
string strNode = params[0].get_str();
|
string strNode = params[0].get_str();
|
||||||
|
|
||||||
// Tor-native: require .onion addresses
|
// Triangles is dual-network Tor + I2P. Allow both .onion and .b32.i2p addresses.
|
||||||
if (strNode.find(".onion") == string::npos)
|
if (strNode.find(".onion") == string::npos && strNode.find(".b32.i2p") == string::npos)
|
||||||
throw runtime_error("Only .onion addresses are supported on this network.");
|
throw runtime_error("Only .onion or .b32.i2p addresses are supported on this network.");
|
||||||
|
|
||||||
if (strCommand == "onetry")
|
if (strCommand == "onetry")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain)
|
|||||||
BOOST_CHECK(Checkpoints::CheckHardened(9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")));
|
BOOST_CHECK(Checkpoints::CheckHardened(9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")));
|
||||||
BOOST_CHECK(Checkpoints::CheckHardened(9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")));
|
BOOST_CHECK(Checkpoints::CheckHardened(9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")));
|
||||||
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
|
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
|
||||||
BOOST_CHECK(Checkpoints::CheckHardened(2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")));
|
// After the operator rollback to 2,172,037 (cycle-32, 2026-08-06), the
|
||||||
BOOST_CHECK(Checkpoints::CheckHardened(2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")));
|
// 2205000/2206004 pins are no longer in the map (those block heights are
|
||||||
|
// above the new canonical tip and reference non-existent blocks). The new
|
||||||
|
// highest entry is 2172037.
|
||||||
|
BOOST_CHECK(Checkpoints::CheckHardened(2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")));
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
|
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
|
||||||
@@ -21,19 +24,23 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
|
|||||||
|
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash));
|
BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash));
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(2205000, wrongHash));
|
BOOST_CHECK(!Checkpoints::CheckHardened(2172037, wrongHash));
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(2206004, wrongHash));
|
|
||||||
|
|
||||||
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
|
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
|
||||||
// pins), so any hash is allowed at those heights.
|
// pins), and after the cycle-32 operator rollback the 2205000+ pins
|
||||||
|
// themselves are gone. Any hash is allowed at those heights.
|
||||||
BOOST_CHECK(Checkpoints::CheckHardened(2186940, wrongHash));
|
BOOST_CHECK(Checkpoints::CheckHardened(2186940, wrongHash));
|
||||||
BOOST_CHECK(Checkpoints::CheckHardened(2186941, 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_CHECK(Checkpoints::CheckHardened(42, wrongHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
|
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
|
||||||
{
|
{
|
||||||
BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 2205000);
|
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returns
|
||||||
|
// 2,172,037 (the new highest compiled checkpoint).
|
||||||
|
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2172037);
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_SUITE_END()
|
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()")
|
BOOST_CHECK(src.find("nHardenedCheckpointHeight = Checkpoints::GetLastCheckpointHeight()")
|
||||||
!= std::string::npos);
|
!= 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
|
// checkpoint height — independent of whether the resolution came
|
||||||
// from the pointer or the compiled map. The literal pattern that
|
// from the pointer or the compiled map. The literal pattern that
|
||||||
// matters is `pfork->nHeight <= nHardenedCheckpointHeight`.
|
// matters is `pfork->nHeight < nHardenedCheckpointHeight`.
|
||||||
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
|
BOOST_CHECK(src.find("pfork->nHeight < nHardenedCheckpointHeight")
|
||||||
!= std::string::npos);
|
!= std::string::npos);
|
||||||
|
|
||||||
// (d) The old guard pattern that short-circuited on the null pointer
|
// (d) The old guard pattern that short-circuited on the null pointer
|
||||||
@@ -699,40 +699,49 @@ BOOST_AUTO_TEST_CASE(reorg_guard_fails_closed_when_checkpoint_pointer_null)
|
|||||||
// ─── Off-by-one hardening: guard operator + bootstrap boundary semantics ──
|
// ─── Off-by-one hardening: guard operator + bootstrap boundary semantics ──
|
||||||
// Adversarial review (Codex round 3 on 6116cff) flagged that the source-grep
|
// Adversarial review (Codex round 3 on 6116cff) flagged that the source-grep
|
||||||
// test reorg_guard_fails_closed_when_checkpoint_pointer_null could let through
|
// 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:
|
// 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()
|
// (ii) the runtime return value of Checkpoints::GetLastCheckpointHeight()
|
||||||
// against the actual compiled map (must equal the highest compiled
|
// against the actual compiled map (must equal the highest compiled
|
||||||
// checkpoint height),
|
// checkpoint height),
|
||||||
// (iii) that the literal RejectReason message uses the "at or below" wording
|
// (iii) that the literal RejectReason message uses the "below" wording
|
||||||
// (matches `<=`).
|
// (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)
|
BOOST_AUTO_TEST_CASE(reorg_guard_offbyone_hardening)
|
||||||
{
|
{
|
||||||
// (i) The guard predicate uses `<=`, NOT `<` or `>=`.
|
// (i) The guard predicate uses `<`, NOT `<=` or `>=`.
|
||||||
// A regression that introduced `pfork->nHeight < nHardenedCheckpointHeight`
|
// A regression that introduced `pfork->nHeight <= nHardenedCheckpointHeight`
|
||||||
// would let a fork exactly at the checkpoint height through.
|
// would cause a permanent chain split when honest nodes see different
|
||||||
|
// height-(checkpoint+1) blocks.
|
||||||
std::string src = readEntireFile("src/main.cpp");
|
std::string src = readEntireFile("src/main.cpp");
|
||||||
BOOST_REQUIRE(!src.empty());
|
BOOST_REQUIRE(!src.empty());
|
||||||
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
|
|
||||||
!= std::string::npos);
|
|
||||||
BOOST_CHECK(src.find("pfork->nHeight < nHardenedCheckpointHeight")
|
BOOST_CHECK(src.find("pfork->nHeight < nHardenedCheckpointHeight")
|
||||||
|
!= std::string::npos);
|
||||||
|
BOOST_CHECK(src.find("pfork->nHeight <= nHardenedCheckpointHeight")
|
||||||
== std::string::npos);
|
== std::string::npos);
|
||||||
BOOST_CHECK(src.find("pfork->nHeight >= nHardenedCheckpointHeight")
|
BOOST_CHECK(src.find("pfork->nHeight >= nHardenedCheckpointHeight")
|
||||||
== std::string::npos);
|
== std::string::npos);
|
||||||
|
|
||||||
// (iii) The reject message wording matches `<=` ("at or below").
|
// (iii) The reject message wording matches `<` ("below").
|
||||||
BOOST_CHECK(src.find("\"REORGANIZE: REJECTED — fork point %d is at or below")
|
BOOST_CHECK(src.find("\"REORGANIZE: REJECTED — fork point %d is below")
|
||||||
!= std::string::npos);
|
!= std::string::npos);
|
||||||
|
|
||||||
// (ii) Runtime: GetLastCheckpointHeight() returns the highest compiled
|
// (ii) Runtime: GetLastCheckpointHeight() returns the highest compiled
|
||||||
// checkpoint height on mainnet. Verified against the actual binary.
|
// checkpoint height on mainnet. Verified against the actual binary.
|
||||||
int nCompiled = Checkpoints::GetLastCheckpointHeight();
|
int nCompiled = Checkpoints::GetLastCheckpointHeight();
|
||||||
BOOST_CHECK(nCompiled > 0); // sanity: compiled map populated
|
BOOST_CHECK(nCompiled > 0); // sanity: compiled map populated
|
||||||
// Must equal the highest key in the compiled map (2224763 as of v6.2.6.0;
|
// Must equal the highest key in the compiled map (2172037 as of cycle-33;
|
||||||
// this assertion locks the value at the time the binary was built, so
|
// this assertion locks the value at the time the binary was built, so
|
||||||
// a regression that drops a checkpoint would also fail here).
|
// a regression that drops a checkpoint would also fail here).
|
||||||
BOOST_CHECK_EQUAL(nCompiled, 2224763);
|
BOOST_CHECK_EQUAL(nCompiled, 2172037);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Duplicate-guard detection: variable referenced only in allowed files ─
|
// ─── Duplicate-guard detection: variable referenced only in allowed files ─
|
||||||
@@ -873,4 +882,46 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
|||||||
"trusting its PASS.");
|
"trusting its PASS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(reindex_reconstruction_is_explicit_and_fail_closed)
|
||||||
|
{
|
||||||
|
// Pin the startup bridge and the fail-closed invariants structurally. The
|
||||||
|
// end-to-end test separately reconstructs the production blk0001.dat;
|
||||||
|
// these checks prevent a refactor from silently returning to the old
|
||||||
|
// "wipe DB, create genesis, never import" behavior.
|
||||||
|
const std::filesystem::path here(__FILE__);
|
||||||
|
const std::filesystem::path root = here.parent_path().parent_path().parent_path();
|
||||||
|
|
||||||
|
std::ifstream initFile(root / "src" / "init.cpp");
|
||||||
|
std::ifstream mainFile(root / "src" / "main.cpp");
|
||||||
|
BOOST_REQUIRE(initFile.good());
|
||||||
|
BOOST_REQUIRE(mainFile.good());
|
||||||
|
|
||||||
|
const std::string initSrc((std::istreambuf_iterator<char>(initFile)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
const std::string mainSrc((std::istreambuf_iterator<char>(mainFile)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
|
||||||
|
BOOST_CHECK(initSrc.find("const bool fReindex = GetBoolArg(\"-reindex\", false)") != std::string::npos);
|
||||||
|
BOOST_CHECK(initSrc.find("if (!FastImportBlockFile())") != std::string::npos);
|
||||||
|
BOOST_CHECK(initSrc.find("else if (!LoadBlockIndex())") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("if (fRequestShutdown)") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("reindex is incomplete") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("interrupted during active-chain replay") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("unexpected or duplicate genesis block") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("invalid record magic") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("malformed block payload") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("trailing bytes at file offset") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("block file was not consumed exactly") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("block failed context-free validation") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("hardened checkpoint mismatch") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("selected chain does not reach the newest compiled checkpoint") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("nFileSize > (int64_t)std::numeric_limits") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("std::unique_ptr<FILE") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("item.second->pnext = nullptr") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("failed to persist best-chain hash") != std::string::npos);
|
||||||
|
BOOST_CHECK(mainSrc.find("final database commit failed") != std::string::npos);
|
||||||
|
BOOST_CHECK(initSrc.find("REINDEX_INCOMPLETE") != std::string::npos);
|
||||||
|
BOOST_CHECK(initSrc.find("SyncReindexMarker") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_SUITE_END()
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
|
|||||||
Reference in New Issue
Block a user