Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d86a4b77fb | |||
| a99e438895 | |||
| a08162d767 | |||
| 3f0fa3aa8a | |||
| 7bb7a5ef93 | |||
| b70725da36 | |||
| f2978ca389 | |||
| 880474065d | |||
| d0506f9e8b |
@@ -87,6 +87,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
|
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--build-arg VERSION=${VERSION} \
|
||||||
--push \
|
--push \
|
||||||
--tag samiahmed7777/trianglesd:$VERSION \
|
--tag samiahmed7777/trianglesd:$VERSION \
|
||||||
--tag samiahmed7777/trianglesd:latest \
|
--tag samiahmed7777/trianglesd:latest \
|
||||||
|
|||||||
+49
-25
@@ -41,22 +41,40 @@ namespace Checkpoints
|
|||||||
// All pins from 2,205,000..2,224,763 have been REMOVED from the map
|
// 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,
|
// (NOT preserved). Their block hashes are not in the canonical chain,
|
||||||
// so leaving them as map entries would let GetTotalBlocksEstimate()
|
// so leaving them as map entries would let GetTotalBlocksEstimate()
|
||||||
// return 2,214,400 — keeping the daemon permanently in IBD because
|
// return a pre-rollback height — keeping the daemon permanently in
|
||||||
// nBestHeight (2,172,037) < 2,214,400. With the operator-rollback
|
// IBD because nBestHeight < GetTotalBlocksEstimate(). The pin at
|
||||||
// pin at 2,172,037 as the new highest entry, GetTotalBlocksEstimate()
|
// 17,650 (line above) remains as the lowest anchored finality
|
||||||
// and GetLastCheckpointHeight() both return 2,172,037, so a node
|
// reference.
|
||||||
// that reaches 2,172,037 exits IBD cleanly. The pin at 17,650
|
// Operator-rollback finality pin (cycle-33, 2026-08-06): the
|
||||||
// (line above) remains as the lowest anchored finality reference.
|
|
||||||
// Operator-rollback finality pin (cycle-33, 2026-08-06): the new
|
|
||||||
// canonical tip after the operator rollback to 2,172,037. Hash
|
// canonical tip after the operator rollback to 2,172,037. Hash
|
||||||
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
|
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
|
||||||
// at canonical tip 2,172,037. This is now the highest entry in
|
// at canonical tip 2,172,037. Was the highest entry from
|
||||||
// mapCheckpoints, so GetTotalBlocksEstimate() returns 2,172,037 and
|
// 2026-08-06 until the 2026-09-02 checkpoint rebase added the
|
||||||
// IsInitialBlockDownload() returns false once a node reaches
|
// pins below; retained as a hardened anchor of the rollback span.
|
||||||
// 2,172,037. Closes the unchecked span between the prior highest
|
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // cycle-33 rollback pin
|
||||||
// pin (17,650) and the new canonical tip for any future
|
// Checkpoint rebase to 2,200,899 (2026-09-02). A strict UTXO
|
||||||
// fresh-from-zero sync.
|
// replay of the full on-disk history (genesis..2,224,763) shows
|
||||||
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // new canonical tip
|
// 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.
|
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||||
@@ -68,17 +86,23 @@ 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 = {
|
||||||
// Historical snapshots preserved as documentation only. The canonical
|
// ONLY the canonical entry may live here. GetBestSnapshotHeight()
|
||||||
// chain is now at 2,172,037 (operator rollback 2026-08-06). Any wallet
|
// returns this map's highest key and DownloadUtxoSnapshot trusts the
|
||||||
// recovering from these old snapshots would also need to bypass the
|
// bootstrap manifest's advertised height when the (height, sha) pair
|
||||||
// chain-state checks via the rollback recipe (see
|
// is present, so a retired entry would let a stale or replayed
|
||||||
// genesis-block-pow-exemption SKILL.md "SAMI-PC wallet recovery recipe"),
|
// manifest hand a fresh wallet an unloadable file. History: the
|
||||||
// which uses the local-file path (utxo-snapshot.bin) with
|
// 2172037 rollback-era snapshot (fc3b2035...) was superseded
|
||||||
// -acceptanylocalsnapshot=1 — that path does NOT enforce the SHA gate.
|
// 2026-09-02 by the checkpoint rebase; the 2200899 Sep-1 dump
|
||||||
// The compiled map below must contain only the canonical snapshot so
|
// (5374ea23...7a) was retired 2026-09-06 — its writer serialization
|
||||||
// GetBestSnapshotHeight() returns 2,172,037 and DownloadUtxoSnapshot
|
// is unreadable by the deployed binaries (CDataStream end-of-data).
|
||||||
// selects the canonical file from bootstrap.cryptographic-triangles.org.
|
// Do NOT re-add retired entries; full history is in git, not in the
|
||||||
{ 2172037, uint256("0xfc3b2035525564156f2489e8929e132b75e9be285d9129ad21bc89ecdc4c7977")}, // canonical
|
// 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 = {
|
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 4
|
#define CLIENT_VERSION_BUILD 7
|
||||||
|
|
||||||
// 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!
|
||||||
|
|||||||
+12
-2
@@ -39,6 +39,16 @@ static fs::path qstringToPath(const QString& s)
|
|||||||
#endif
|
#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) :
|
IntroDialog::IntroDialog(QWidget *parent) :
|
||||||
QDialog(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)
|
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||||
QString defaultDir = QString::fromStdString(
|
QString defaultDir = QString::fromStdString(
|
||||||
std::string(GetDefaultDataDir().u8string()));
|
pathToUtf8String(GetDefaultDataDir()));
|
||||||
if (dataDir != defaultDir) {
|
if (dataDir != defaultDir) {
|
||||||
// Pass the data dir to the daemon as UTF-8 bytes so a non-ASCII path
|
// 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
|
// on Windows isn't mangled by the ANSI code page (path::string() does
|
||||||
// that). The daemon side uses fs::u8path() to convert back.
|
// 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
|
// 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).
|
// 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
|
// 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
|
// 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
|
// above the rollback tip and reference non-existent blocks). The rebase
|
||||||
// highest entry is 2172037.
|
// 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(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)
|
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(9000, wrongHash));
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
||||||
BOOST_CHECK(!Checkpoints::CheckHardened(2172037, 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+
|
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
|
||||||
// pins), and after the cycle-32 operator rollback the 2205000+ pins
|
// pins, which were themselves removed in the cycle-32 operator
|
||||||
// themselves are gone. Any hash is allowed at those heights.
|
// rollback). 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(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)
|
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
|
||||||
{
|
{
|
||||||
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returns
|
// After operator rollback to 2,172,037, GetTotalBlocksEstimate() returned
|
||||||
// 2,172,037 (the new highest compiled checkpoint).
|
// 2,172,037. Since the rebase snapshot anchor (2026-09-06), the highest
|
||||||
BOOST_CHECK_EQUAL(Checkpoints::GetTotalBlocksEstimate(), 2172037);
|
// 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()
|
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.
|
// 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 (2172037 as of cycle-33;
|
// Must equal the highest key in the compiled map (2201018 since the
|
||||||
// this assertion locks the value at the time the binary was built, so
|
// 2026-09-06 rebase snapshot anchor; was 2200899 from the 2026-09-02
|
||||||
// a regression that drops a checkpoint would also fail here).
|
// rebase; this assertion locks the value at the time the binary was
|
||||||
BOOST_CHECK_EQUAL(nCompiled, 2172037);
|
// 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 ─
|
// ─── Duplicate-guard detection: variable referenced only in allowed files ─
|
||||||
|
|||||||
+98
-8
@@ -671,6 +671,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
unsigned int nPos = 0;
|
unsigned int nPos = 0;
|
||||||
unsigned int nBlocksIndexed = 0;
|
unsigned int nBlocksIndexed = 0;
|
||||||
unsigned int nTxsIndexed = 0;
|
unsigned int nTxsIndexed = 0;
|
||||||
|
unsigned int nVerified = 0;
|
||||||
unsigned int nBatchTxs = 0;
|
unsigned int nBatchTxs = 0;
|
||||||
int64_t nLastReport = GetTimeMillis();
|
int64_t nLastReport = GetTimeMillis();
|
||||||
while (success && blkdat.good()) {
|
while (success && blkdat.good()) {
|
||||||
@@ -698,20 +699,108 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
}
|
}
|
||||||
CBlock block;
|
CBlock block;
|
||||||
blkdat >> block;
|
blkdat >> block;
|
||||||
|
// Stored block position: header start (post magic+size),
|
||||||
|
// the reader convention. Declared here so both the tx
|
||||||
|
// loop (CDiskTxPos) and the post-loop readback use the
|
||||||
|
// SAME stored constant.
|
||||||
|
const unsigned int nBlockPosStored =
|
||||||
|
nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int);
|
||||||
// For each tx in the block, record the disk position.
|
// For each tx in the block, record the disk position.
|
||||||
// nTxPos is the offset of the tx *within* the block (after
|
// ConnectBlock (main.cpp) computes the first tx as
|
||||||
// magic+size for the first tx, then serialize-size of
|
// nBlockPos + GetSerializeSize(CBlock())
|
||||||
// preceding txs). We use the post-serialize offset of each
|
// - 2*GetSizeOfCompactSize(0)
|
||||||
// tx as nTxPos, matching the convention in ConnectBlock.
|
// + GetSizeOfCompactSize(vtx.size())
|
||||||
unsigned int nTxPos = sizeof(pchMessageStart) + sizeof(unsigned int); // offset of first tx in block
|
// 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) {
|
for (const CTransaction& tx : block.vtx) {
|
||||||
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
|
// nBlockPos must point at the HEADER (post
|
||||||
|
// magic+size), the codebase-wide convention:
|
||||||
|
// CBlock::ReadFromDisk(nFile, nBlockPos) seeks to
|
||||||
|
// nBlockPos and deserializes the header directly.
|
||||||
|
// Storing the magic position (8 bytes early) makes
|
||||||
|
// CheckProofOfStake read a garbage header whose hash
|
||||||
|
// is absent from mapBlockIndex — the
|
||||||
|
// "GetKernelStakeModifier() : block not indexed" +
|
||||||
|
// "check kernel failed" rejection of every
|
||||||
|
// post-snapshot PoS block.
|
||||||
|
CDiskTxPos posThisTx(1, nBlockPosStored, nTxPos);
|
||||||
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
|
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
|
||||||
|
// Fail-closed readback (added after the 2026-09-06
|
||||||
|
// txpos incidents): for every 4096th tx, immediately
|
||||||
|
// re-read it at the position we just wrote and prove
|
||||||
|
// the round-trip. A wrong offset convention here
|
||||||
|
// previously produced a "successfully loaded" node
|
||||||
|
// that silently rejected every post-snapshot PoS
|
||||||
|
// block. Only the in-memory tx is consulted for the
|
||||||
|
// comparison — a mismatch means our position math or
|
||||||
|
// blk0001.dat extraction is wrong, and the load fails.
|
||||||
|
if ((nTxsIndexed % 4096) == 0) {
|
||||||
|
fseek(blkdat, nTxPos, SEEK_SET);
|
||||||
|
CTransaction txReadback;
|
||||||
|
bool fReadOK = true;
|
||||||
|
try {
|
||||||
|
blkdat >> txReadback;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
fReadOK = false;
|
||||||
|
}
|
||||||
|
if (!fReadOK || txReadback.GetHash() != tx.GetHash()) {
|
||||||
|
success = false;
|
||||||
|
strError = "txindex readback verification failed at block "
|
||||||
|
+ block.GetHash().ToString().substr(0, 16)
|
||||||
|
+ " tx " + tx.GetHash().ToString().substr(0, 16)
|
||||||
|
+ " — loader offset bug or corrupt blk0001.dat; "
|
||||||
|
"NOT announcing a verified load";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nVerified++;
|
||||||
|
}
|
||||||
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||||
nTxsIndexed++;
|
nTxsIndexed++;
|
||||||
nBatchTxs++;
|
nBatchTxs++;
|
||||||
}
|
}
|
||||||
nBlocksIndexed++;
|
nBlocksIndexed++;
|
||||||
|
// Fail-closed block-level readback (added after the
|
||||||
|
// 2026-09-06 txpos incidents): starting with the very
|
||||||
|
// first block, then every 512th, prove the STORED
|
||||||
|
// position convention by reading through the ACTUAL
|
||||||
|
// reader path with the position exactly as written into
|
||||||
|
// the txindex — CBlock::ReadFromDisk(nBlockPosStored,
|
||||||
|
// false) seeks to that position and deserializes the
|
||||||
|
// header directly, exactly as CheckProofOfStake does for
|
||||||
|
// stake inputs. A wrong nBlockPos convention (magic vs
|
||||||
|
// header) previously produced a "successfully loaded"
|
||||||
|
// node whose kernel checks all failed; reading an
|
||||||
|
// independently recomputed position instead of the
|
||||||
|
// stored one would miss that class of bug, so the stored
|
||||||
|
// constant itself is the source here.
|
||||||
|
if (nBlocksIndexed == 1 || (nBlocksIndexed % 512) == 0) {
|
||||||
|
CBlock blockReadback;
|
||||||
|
if (!blockReadback.ReadFromDisk(1,
|
||||||
|
nBlockPosStored,
|
||||||
|
false)
|
||||||
|
|| blockReadback.GetHash() != block.GetHash()) {
|
||||||
|
success = false;
|
||||||
|
strError = "txindex block readback verification failed at "
|
||||||
|
+ block.GetHash().ToString().substr(0, 16)
|
||||||
|
+ " — stored nBlockPos does not honor the "
|
||||||
|
"header-at-pos reader convention; NOT "
|
||||||
|
"announcing a verified load";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nVerified++;
|
||||||
|
}
|
||||||
// Advance past this block to scan the next one
|
// Advance past this block to scan the next one
|
||||||
nPos = nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int) + nSize;
|
nPos = nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int) + nSize;
|
||||||
// Commit batch periodically to avoid unbounded memory
|
// Commit batch periodically to avoid unbounded memory
|
||||||
@@ -739,8 +828,9 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
strError = "Final txindex commit failed";
|
strError = "Final txindex commit failed";
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions\n",
|
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions "
|
||||||
nBlocksIndexed, nTxsIndexed);
|
"(%u readback-verified)\n",
|
||||||
|
nBlocksIndexed, nTxsIndexed, nVerified);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user