Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bb7a5ef93 | |||
| b70725da36 | |||
| f2978ca389 | |||
| 880474065d | |||
| d0506f9e8b |
+49
-25
@@ -41,22 +41,40 @@ namespace Checkpoints
|
||||
// All pins from 2,205,000..2,224,763 have been REMOVED from the map
|
||||
// (NOT preserved). Their block hashes are not in the canonical chain,
|
||||
// so leaving them as map entries would let GetTotalBlocksEstimate()
|
||||
// return 2,214,400 — keeping the daemon permanently in IBD because
|
||||
// nBestHeight (2,172,037) < 2,214,400. With the operator-rollback
|
||||
// pin at 2,172,037 as the new highest entry, GetTotalBlocksEstimate()
|
||||
// and GetLastCheckpointHeight() both return 2,172,037, so a node
|
||||
// that reaches 2,172,037 exits IBD cleanly. The pin at 17,650
|
||||
// (line above) remains as the lowest anchored finality reference.
|
||||
// Operator-rollback finality pin (cycle-33, 2026-08-06): the new
|
||||
// return a pre-rollback height — keeping the daemon permanently in
|
||||
// IBD because nBestHeight < GetTotalBlocksEstimate(). The pin at
|
||||
// 17,650 (line above) remains as the lowest anchored finality
|
||||
// reference.
|
||||
// Operator-rollback finality pin (cycle-33, 2026-08-06): the
|
||||
// canonical tip after the operator rollback to 2,172,037. Hash
|
||||
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
|
||||
// at canonical tip 2,172,037. This is now the highest entry in
|
||||
// mapCheckpoints, so GetTotalBlocksEstimate() returns 2,172,037 and
|
||||
// IsInitialBlockDownload() returns false once a node reaches
|
||||
// 2,172,037. Closes the unchecked span between the prior highest
|
||||
// pin (17,650) and the new canonical tip for any future
|
||||
// fresh-from-zero sync.
|
||||
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // new canonical tip
|
||||
// at canonical tip 2,172,037. Was the highest entry from
|
||||
// 2026-08-06 until the 2026-09-02 checkpoint rebase added the
|
||||
// pins below; retained as a hardened anchor of the rollback span.
|
||||
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // cycle-33 rollback pin
|
||||
// Checkpoint rebase to 2,200,899 (2026-09-02). A strict UTXO
|
||||
// replay of the full on-disk history (genesis..2,224,763) shows
|
||||
// that heights 2,172,038..2,200,899 validate cleanly, while the
|
||||
// canonical chain from height 2,200,900 (2026-04-07) onward
|
||||
// contains 805 coinstake inputs (in 603 blocks) that re-spend
|
||||
// outputs already spent by earlier blocks — accepted at the time
|
||||
// only because of the v5.8.x vSpent tracking bug. No correct
|
||||
// node can ever validate that span, so 2,200,899 is the last
|
||||
// block that can be canonical. Pins below restore 10k-block
|
||||
// spacing across the recovered span. Hashes computed directly
|
||||
// from blk0001.dat headers (X13) and cross-checked against the
|
||||
// chain that all live-network pins (2,222,900..2,224,763) sat on.
|
||||
{ 2180000, uint256("0xe3d2780d838314cb759784757e7e84cd0f18a46d333d3e6aaa4f79d5060104a0")},
|
||||
{ 2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{ 2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{ 2200500, uint256("0x68fd5eedbefe80431fba92ee4ea37993f3e5f22f88b38a564e582a5c4aa15db2")},
|
||||
{ 2200899, uint256("0x28e57e03c7f48df8ef0dedba2b93fd5176500729c955f86546c381be66952e55")}, // rebase base (last clean block)
|
||||
// Rebase snapshot anchor (2026-09-06): the published canonical
|
||||
// snapshot tip. Hash verified live via sami-pc getblockhash and
|
||||
// byte-reversed against utxo-snapshot-2201018.utx's internal
|
||||
// header blockhash. Highest pin: GetTotalBlocksEstimate()
|
||||
// returns 2,201,018.
|
||||
{ 2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")}, // canonical tip (rebase snapshot anchor)
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
@@ -68,17 +86,23 @@ namespace Checkpoints
|
||||
// here. The corresponding (height, blockhash) must already exist in
|
||||
// mapCheckpoints / mapCheckpointsTestnet.
|
||||
static std::map<int, uint256> mapSnapshotHashes = {
|
||||
// Historical snapshots preserved as documentation only. The canonical
|
||||
// chain is now at 2,172,037 (operator rollback 2026-08-06). Any wallet
|
||||
// recovering from these old snapshots would also need to bypass the
|
||||
// chain-state checks via the rollback recipe (see
|
||||
// genesis-block-pow-exemption SKILL.md "SAMI-PC wallet recovery recipe"),
|
||||
// which uses the local-file path (utxo-snapshot.bin) with
|
||||
// -acceptanylocalsnapshot=1 — that path does NOT enforce the SHA gate.
|
||||
// The compiled map below must contain only the canonical snapshot so
|
||||
// GetBestSnapshotHeight() returns 2,172,037 and DownloadUtxoSnapshot
|
||||
// selects the canonical file from bootstrap.cryptographic-triangles.org.
|
||||
{ 2172037, uint256("0xfc3b2035525564156f2489e8929e132b75e9be285d9129ad21bc89ecdc4c7977")}, // canonical
|
||||
// ONLY the canonical entry may live here. GetBestSnapshotHeight()
|
||||
// returns this map's highest key and DownloadUtxoSnapshot trusts the
|
||||
// bootstrap manifest's advertised height when the (height, sha) pair
|
||||
// is present, so a retired entry would let a stale or replayed
|
||||
// manifest hand a fresh wallet an unloadable file. History: the
|
||||
// 2172037 rollback-era snapshot (fc3b2035...) was superseded
|
||||
// 2026-09-02 by the checkpoint rebase; the 2200899 Sep-1 dump
|
||||
// (5374ea23...7a) was retired 2026-09-06 — its writer serialization
|
||||
// is unreadable by the deployed binaries (CDataStream end-of-data).
|
||||
// Do NOT re-add retired entries; full history is in git, not in the
|
||||
// live trust-anchor map.
|
||||
// Canonical rebase snapshot (2026-09-06): dumped live from the
|
||||
// staking node (sami-pc, deployed binary v6.2.6.4), published at
|
||||
// bootstrap.cryptographic-triangles.org/utxo-snapshot.bin with
|
||||
// manifest v3.0. Load-verified end-to-end on DNS2 (all 2,201,019
|
||||
// headers + 17,720 UTXOs + txindex rebuild).
|
||||
{ 2201018, uint256("0xed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab")}, // canonical (only entry)
|
||||
};
|
||||
|
||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
#define CLIENT_VERSION_MAJOR 6
|
||||
#define CLIENT_VERSION_MINOR 2
|
||||
#define CLIENT_VERSION_REVISION 6
|
||||
#define CLIENT_VERSION_BUILD 4
|
||||
#define CLIENT_VERSION_BUILD 6
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
// Don't merge these into one macro!
|
||||
|
||||
+12
-2
@@ -39,6 +39,16 @@ static fs::path qstringToPath(const QString& s)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Convert fs::path to a UTF-8 std::string. fs::path::u8string() returns
|
||||
// std::u8string in C++20, which has no implicit conversion to std::string
|
||||
// (clang/gcc reject the functional cast). Reinterpret the char8_t payload:
|
||||
// UTF-8 byte values are preserved exactly.
|
||||
static std::string pathToUtf8String(const fs::path& p)
|
||||
{
|
||||
const std::u8string u8 = p.u8string();
|
||||
return std::string(reinterpret_cast<const char*>(u8.data()), u8.size());
|
||||
}
|
||||
|
||||
IntroDialog::IntroDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
@@ -233,12 +243,12 @@ bool IntroDialog::pickDataDirectory()
|
||||
|
||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||
QString defaultDir = QString::fromStdString(
|
||||
std::string(GetDefaultDataDir().u8string()));
|
||||
pathToUtf8String(GetDefaultDataDir()));
|
||||
if (dataDir != defaultDir) {
|
||||
// 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());
|
||||
mapArgs["-datadir"] = pathToUtf8String(qstringToPath(dataDir));
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
|
||||
@@ -13,9 +13,12 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain)
|
||||
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
|
||||
// After the operator rollback to 2,172,037 (cycle-32, 2026-08-06), the
|
||||
// 2205000/2206004 pins are no longer in the map (those block heights are
|
||||
// above the new canonical tip and reference non-existent blocks). The new
|
||||
// highest entry is 2172037.
|
||||
// above the rollback tip and reference non-existent blocks). The rebase
|
||||
// base pin (2200899) and the rebase snapshot anchor (2201018, added
|
||||
// 2026-09-06) are the highest entries.
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2200899, uint256("0x28e57e03c7f48df8ef0dedba2b93fd5176500729c955f86546c381be66952e55")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
|
||||
@@ -25,10 +28,14 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2172037, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2200899, wrongHash));
|
||||
// Negative assertion for the rebase snapshot anchor pin (2026-09-06):
|
||||
// the height is hardened, so a wrong hash must be rejected.
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2201018, wrongHash));
|
||||
|
||||
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
|
||||
// pins), and after the cycle-32 operator rollback the 2205000+ pins
|
||||
// themselves are gone. Any hash is allowed at those heights.
|
||||
// pins, which were themselves removed in the cycle-32 operator
|
||||
// rollback). Any hash is allowed at those heights.
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2186940, wrongHash));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2186941, wrongHash));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2205000, wrongHash));
|
||||
@@ -38,9 +45,35 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
|
||||
|
||||
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
|
||||
{
|
||||
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returns
|
||||
// 2,172,037 (the new highest compiled checkpoint).
|
||||
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2172037);
|
||||
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returned
|
||||
// 2,172,037. Since the rebase snapshot anchor (2026-09-06), the highest
|
||||
// compiled checkpoint is 2,201,018.
|
||||
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2201018);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(best_snapshot_is_canonical_rebase_snapshot)
|
||||
{
|
||||
// The auto-download path (DownloadUtxoSnapshot) selects whatever
|
||||
// GetBestSnapshotHeight() returns and enforces the compiled (height, sha)
|
||||
// pair from mapSnapshotHashes. Lock both to the canonical rebase snapshot
|
||||
// (2026-09-06) so a retired entry can never be re-selected and a stale or
|
||||
// replayed bootstrap manifest cannot satisfy the gate with an old file.
|
||||
BOOST_CHECK_EQUAL(Checkpoints::GetBestSnapshotHeight(), 2201018);
|
||||
uint256 fileHash;
|
||||
BOOST_CHECK(Checkpoints::GetSnapshotHash(2201018, fileHash));
|
||||
BOOST_CHECK_EQUAL(fileHash.GetHex(),
|
||||
"ed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab");
|
||||
// The retired snapshots must NOT be selectable: the 2172037 rollback-era
|
||||
// snapshot (removed 2026-09-06) and the 2200899 Sep-1 dump (writer/reader
|
||||
// serialization mismatch — unloadable on deployed binaries).
|
||||
BOOST_CHECK(!Checkpoints::GetSnapshotHash(2172037, fileHash));
|
||||
BOOST_CHECK(!Checkpoints::GetSnapshotHash(2200899, fileHash));
|
||||
// Cross-map invariant: the best snapshot height must sit on a hardened
|
||||
// checkpoint whose block hash matches the published snapshot's tip. This
|
||||
// prevents future snapshot/checkpoint drift — the two maps are written
|
||||
// together, and DownloadUtxoSnapshot requires BOTH gates to pass.
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(
|
||||
2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -738,10 +738,11 @@ BOOST_AUTO_TEST_CASE(reorg_guard_offbyone_hardening)
|
||||
// checkpoint height on mainnet. Verified against the actual binary.
|
||||
int nCompiled = Checkpoints::GetLastCheckpointHeight();
|
||||
BOOST_CHECK(nCompiled > 0); // sanity: compiled map populated
|
||||
// Must equal the highest key in the compiled map (2172037 as of cycle-33;
|
||||
// this assertion locks the value at the time the binary was built, so
|
||||
// a regression that drops a checkpoint would also fail here).
|
||||
BOOST_CHECK_EQUAL(nCompiled, 2172037);
|
||||
// Must equal the highest key in the compiled map (2201018 since the
|
||||
// 2026-09-06 rebase snapshot anchor; was 2200899 from the 2026-09-02
|
||||
// rebase; this assertion locks the value at the time the binary was
|
||||
// built, so a regression that drops a checkpoint would also fail here).
|
||||
BOOST_CHECK_EQUAL(nCompiled, 2201018);
|
||||
}
|
||||
|
||||
// ─── Duplicate-guard detection: variable referenced only in allowed files ─
|
||||
|
||||
+18
-5
@@ -699,11 +699,24 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
CBlock block;
|
||||
blkdat >> block;
|
||||
// For each tx in the block, record the disk position.
|
||||
// nTxPos is the offset of the tx *within* the block (after
|
||||
// magic+size for the first tx, then serialize-size of
|
||||
// preceding txs). We use the post-serialize offset of each
|
||||
// tx as nTxPos, matching the convention in ConnectBlock.
|
||||
unsigned int nTxPos = sizeof(pchMessageStart) + sizeof(unsigned int); // offset of first tx in block
|
||||
// ConnectBlock (main.cpp) computes the first tx as
|
||||
// nBlockPos + GetSerializeSize(CBlock())
|
||||
// - 2*GetSizeOfCompactSize(0)
|
||||
// + GetSizeOfCompactSize(vtx.size())
|
||||
// where nBlockPos points just AFTER the magic+size prefix
|
||||
// (i.e. at the 80-byte header). Here nBlockStart points
|
||||
// AT the magic, so add the 8-byte prefix first:
|
||||
// first tx = nBlockStart + 8 + 80 + compactsize(vtx)
|
||||
// The old code forgot the 80-byte header (started txs at
|
||||
// +8), shifting every txindex entry 81 bytes low and
|
||||
// making ReadFromDisk desync — "read txPrev failed" —
|
||||
// which rejected all post-snapshot PoS blocks and froze
|
||||
// snapshot-loaded nodes at the snapshot tip.
|
||||
unsigned int nTxPos = nBlockStart
|
||||
+ sizeof(pchMessageStart) + sizeof(unsigned int)
|
||||
+ ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
|
||||
- (2 * GetSizeOfCompactSize(0))
|
||||
+ GetSizeOfCompactSize(block.vtx.size());
|
||||
for (const CTransaction& tx : block.vtx) {
|
||||
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
|
||||
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
|
||||
|
||||
Reference in New Issue
Block a user