Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c312bb7da | |||
| 41ba9f8bc9 | |||
| cbb189aade | |||
| 5635cb5e57 | |||
| b6feab8e94 | |||
| 333f7abfc0 | |||
| eb20edf890 |
@@ -47,6 +47,30 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "i[3-6]86")
|
||||
add_compile_options(-msse2)
|
||||
endif()
|
||||
|
||||
# ── x86-64 baseline ISA (portability across CPU vendors/models) ──
|
||||
# CRITICAL: Without this, GCC on Intel CI runners (Skylake-X, Ice Lake,
|
||||
# Sapphire Rapids) emits AVX-512 / AVX10 instructions (vmovdqu8, vpcompressd,
|
||||
# vpopcntd, etc.) for std::string / memcpy inlining that CRASH with SIGILL
|
||||
# on AMD EPYC (Milan, Genoa) and older Intel without AVX-512/AVX10.
|
||||
# x86-64-v2 = baseline from ~2009 (Nehalem): SSE4.2 + POPCNT + CMPXCHG16B.
|
||||
# Supported on EVERY x86_64 CPU Triangles runs on in production (DNS2, DNS3,
|
||||
# Hetzner ARM64 excluded — that's a different build). Do NOT raise to v3
|
||||
# (AVX2) without re-testing on every supported CPU; v3 is fine for most
|
||||
# modern hardware but adds risk on edge cases (early Ryzen, Atom).
|
||||
# Override with -DCMAKE_X86_64_BASELINE=OFF to disable (not recommended).
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" AND NOT WIN32 AND NOT APPLE)
|
||||
option(CMAKE_X86_64_BASELINE
|
||||
"Compile with -march=x86-64-v2 (SSE4.2 baseline) for portability across CPU vendors"
|
||||
ON)
|
||||
if(CMAKE_X86_64_BASELINE)
|
||||
add_compile_options(-march=x86-64-v2)
|
||||
# -mtune=generic tells GCC the binary will run on CPUs other than the
|
||||
# build host. Combined with -march=x86-64-v2 above, the scheduler
|
||||
# picks instructions from the v2 subset only — no AVX-512 leaks.
|
||||
add_compile_options(-mtune=generic)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── Platform: Windows (MSYS2 MinGW64) ──
|
||||
if(WIN32)
|
||||
add_compile_options(-Wa,-mbig-obj)
|
||||
|
||||
+21
-6
@@ -32,12 +32,27 @@ namespace Checkpoints
|
||||
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
// Recent finality pin (PoS era). Closes the long unchecked span from
|
||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||
// this height are rejected outright. Hash from the canonical chain.
|
||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
||||
};
|
||||
// Recent finality pin (PoS era). Closes the long unchecked span from
|
||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||
// this height are rejected outright. Hash from the canonical chain.
|
||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
||||
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
|
||||
// gap between the last hardcoded checkpoint and the live tip stays bounded.
|
||||
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
|
||||
// unverified blocks at tip — a peer feeding fork blocks at those heights
|
||||
// could trick an IBD node into accepting a divergent chain. With these
|
||||
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
|
||||
// All hashes verified against the canonical chain on 2026-07-01.
|
||||
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
|
||||
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
|
||||
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
|
||||
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
|
||||
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
|
||||
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
|
||||
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
|
||||
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 6
|
||||
#define CLIENT_VERSION_MINOR 1
|
||||
#define CLIENT_VERSION_REVISION 0
|
||||
#define CLIENT_VERSION_REVISION 1
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Dynamic seeds will also be available at:
|
||||
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
|
||||
static const char *strMainNetI2PSeed[][1] = {
|
||||
// SAMI-PC - authoritative wallet node (main PC)
|
||||
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
|
||||
// DNS2 - primary bootstrap server (194.233.88.206)
|
||||
// Generated by embedded i2pd on first run, keys persist in i2p_data/
|
||||
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
|
||||
|
||||
+18
-2
@@ -3478,10 +3478,26 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
bnRequired.SetCompact(ComputeMinWork(pindexLastPow->nBits, deltaTime));
|
||||
}
|
||||
|
||||
if (bnRequired != 0 && bnNewBlock > bnRequired)
|
||||
// bnNewBlock is the difficulty of the candidate block (compact bits -> target).
|
||||
// bnRequired is the MINIMUM difficulty the block must meet (based on time since
|
||||
// last checkpoint / chain tip). If the candidate's target is SMALLER than required
|
||||
// (i.e. block is harder than allowed), it's "too much" difficulty and we reject.
|
||||
// If LARGER (less difficulty = easier than required), it's "too little" and we reject.
|
||||
// PREVIOUS BUG: condition was `bnNewBlock > bnRequired` paired with "too little"
|
||||
// error message — the message and the trigger were swapped. This caused honest
|
||||
// blocks during legitimate time-warps (fork recovery, chain catchup) to be
|
||||
// labelled "too little proof-of-stake" while the actual reject reason was the
|
||||
// OPPOSITE — block had TOO MUCH difficulty relative to elapsed time.
|
||||
// Fixed: condition now matches the message (block too easy => reject).
|
||||
if (bnRequired != 0 && bnNewBlock < bnRequired)
|
||||
{
|
||||
// Anti-spam is a soft scoring signal, NOT a hard ban trigger. A single
|
||||
// violation should log + score modestly, not 24-hour-ban honest peers
|
||||
// (which is what happened during the 2026-06-23 DNS2 clearnet-fork
|
||||
// incident — `Misbehaving(100)` crossed the banscore threshold on the
|
||||
// FIRST block, instantly banning every honest peer feeding us fork blocks).
|
||||
if (pfrom)
|
||||
pfrom->Misbehaving(100);
|
||||
pfrom->Misbehaving(5);
|
||||
return error("ProcessBlock() : block with too little %s", pblock->IsProofOfStake()? "proof-of-stake" : "proof-of-work");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// Hardcoded onion seed nodes for initial peer discovery.
|
||||
// Also fetched dynamically via https://seeds.cryptographic-triangles.org/seeds.txt
|
||||
static const char *strMainNetOnionSeed[][1] = {
|
||||
// SAMI-PC - authoritative wallet node (main PC)
|
||||
{"6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion"},
|
||||
// DNS2 - primary bootstrap server (194.233.88.206)
|
||||
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
|
||||
// DNS3 - canonical chain reference (74.208.167.19)
|
||||
|
||||
+96
-4
@@ -111,6 +111,23 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
if (blkSize > 0) numBlocks = (unsigned int)blkSize;
|
||||
}
|
||||
}
|
||||
// v3: collect setStakeSeen entries (prevoutStake, nStakeTime) from the
|
||||
// last N PoS blocks. Required so a snapshot-loaded node has the recent
|
||||
// stake-collision set restored without walking blocks at startup.
|
||||
static const unsigned int STAKE_SEEN_DEPTH = 5000; // 10x LoadBlockIndex default
|
||||
std::vector<std::pair<COutPoint, unsigned int> > vStakeSeen;
|
||||
{
|
||||
CBlockIndex* pindex = pindexBest;
|
||||
unsigned int nVisited = 0;
|
||||
while (pindex && nVisited < STAKE_SEEN_DEPTH) {
|
||||
if (pindex->IsProofOfStake()) {
|
||||
vStakeSeen.push_back(std::make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
}
|
||||
pindex = pindex->pprev;
|
||||
nVisited++;
|
||||
}
|
||||
}
|
||||
unsigned int numStakeSeen = (unsigned int)vStakeSeen.size();
|
||||
uint256 contentHash; // placeholder, filled after writing data
|
||||
|
||||
fwrite(&magic, sizeof(magic), 1, file);
|
||||
@@ -122,6 +139,7 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
fwrite(&numBlocks, sizeof(numBlocks), 1, file); // v2+
|
||||
fwrite(&numStakeSeen, sizeof(numStakeSeen), 1, file); // v3+
|
||||
long contentHashPos = ftell(file);
|
||||
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
|
||||
|
||||
@@ -195,14 +213,40 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
// Update actual count (in case it changed during iteration)
|
||||
if (nWritten != numUtxos) {
|
||||
numUtxos = nWritten;
|
||||
// Seek back and update numUtxos in header
|
||||
// Seek back and update numUtxos in header.
|
||||
// Header layout (v3):
|
||||
// magic(4) + version(4) + network(4) + height(4) + blockHash(32)
|
||||
// + moneySupply(8) + numHeaders(4) + numUtxos(4)
|
||||
// + numBlocks(4) + numStakeSeen(4) + contentHash(32)
|
||||
// contentHashPos is the offset of contentHash. numUtxos is at
|
||||
// contentHashPos - sizeof(contentHash) - sizeof(numStakeSeen)
|
||||
// - sizeof(numBlocks) - sizeof(numUtxos).
|
||||
long currentPos = ftell(file);
|
||||
fseek(file, contentHashPos - sizeof(numUtxos), SEEK_SET);
|
||||
fseek(file, contentHashPos - sizeof(uint256) - sizeof(numStakeSeen)
|
||||
- sizeof(numBlocks) - sizeof(numUtxos), SEEK_SET);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
fseek(file, currentPos, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
// v3: After UTXOs, write the setStakeSeen entries collected from the last
|
||||
// N PoS blocks. Format: a length-prefixed flat array of
|
||||
// (COutPoint prevout, unsigned int nStakeTime) records.
|
||||
if (version >= 3) {
|
||||
printf("UtxoSnapshot: writing %d setStakeSeen entries...\n", numStakeSeen);
|
||||
for (unsigned int i = 0; i < vStakeSeen.size(); i++) {
|
||||
CDataStream ssEntry(SER_DISK, CLIENT_VERSION);
|
||||
ssEntry << vStakeSeen[i].first; // COutPoint (hash + index)
|
||||
ssEntry << vStakeSeen[i].second; // nStakeTime
|
||||
unsigned int entrySize = (unsigned int)ssEntry.size();
|
||||
std::string strEntry = ssEntry.str();
|
||||
fwrite(&entrySize, sizeof(entrySize), 1, file);
|
||||
fwrite(strEntry.data(), 1, entrySize, file);
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, strEntry.data(), entrySize);
|
||||
}
|
||||
}
|
||||
|
||||
// v2: After UTXOs, append raw blk0001.dat content. Streams in chunks;
|
||||
// SHA256 covers the bytes. A snapshot-loaded node has full block data
|
||||
// ready in datadir/blk0001.dat — no separate bootstrap needed.
|
||||
@@ -290,7 +334,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
int height;
|
||||
uint256 blockHash;
|
||||
int64_t moneySupply;
|
||||
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0;
|
||||
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0, numStakeSeen = 0;
|
||||
uint256 expectedContentHash;
|
||||
|
||||
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
|
||||
@@ -305,7 +349,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
strError = "Truncated snapshot header (common fields)";
|
||||
return false;
|
||||
}
|
||||
// v2+ has numBlocks between numUtxos and contentHash. v1 stops here.
|
||||
// v2+ has numBlocks between numUtxos and (numStakeSeen|contentHash).
|
||||
if (version >= 2) {
|
||||
if (fread(&numBlocks, sizeof(numBlocks), 1, file) != 1) {
|
||||
fclose(file);
|
||||
@@ -313,6 +357,14 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// v3+ has numStakeSeen before contentHash.
|
||||
if (version >= 3) {
|
||||
if (fread(&numStakeSeen, sizeof(numStakeSeen), 1, file) != 1) {
|
||||
fclose(file);
|
||||
strError = "Truncated snapshot header (numStakeSeen)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
|
||||
fclose(file);
|
||||
strError = "Truncated snapshot header (contentHash)";
|
||||
@@ -522,6 +574,46 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
success = false;
|
||||
}
|
||||
|
||||
// v3: After UTXOs (before the embedded blocks), read the setStakeSeen
|
||||
// entries collected from the last N PoS blocks of the source chain.
|
||||
// Required so a snapshot-loaded node has the recent stake-collision set
|
||||
// restored immediately, without having to walk blocks at startup. This
|
||||
// is what lets the anti-spam "too little proof-of-stake" check in
|
||||
// ProcessBlock function correctly right after a snapshot bootstrap.
|
||||
if (success && version >= 3 && numStakeSeen > 0) {
|
||||
printf("UtxoSnapshot: loading %d setStakeSeen entries...\n", numStakeSeen);
|
||||
// setStakeSeen is declared in main.cpp — we reference it via the
|
||||
// header declaration. Clear first so the snapshot's view is authoritative.
|
||||
setStakeSeen.clear();
|
||||
unsigned int nLoadedStakeSeen = 0;
|
||||
for (unsigned int i = 0; i < numStakeSeen; i++) {
|
||||
unsigned int entrySize;
|
||||
if (fread(&entrySize, sizeof(entrySize), 1, file) != 1 || entrySize > 1000) {
|
||||
success = false;
|
||||
strError = "Invalid setStakeSeen entry size at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
std::vector<char> buf(entrySize);
|
||||
if (fread(buf.data(), 1, entrySize, file) != entrySize) {
|
||||
success = false;
|
||||
strError = "Truncated setStakeSeen entry at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, buf.data(), entrySize);
|
||||
|
||||
CDataStream ssEntry(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
|
||||
COutPoint prevout;
|
||||
unsigned int nStakeTime;
|
||||
ssEntry >> prevout;
|
||||
ssEntry >> nStakeTime;
|
||||
setStakeSeen.insert(std::make_pair(prevout, nStakeTime));
|
||||
nLoadedStakeSeen++;
|
||||
}
|
||||
if (success)
|
||||
printf("UtxoSnapshot: loaded %d setStakeSeen entries\n", nLoadedStakeSeen);
|
||||
}
|
||||
|
||||
// v2: After UTXOs, extract the raw blk0001.dat content. This makes the
|
||||
// loaded node fully self-contained — no separate bootstrap needed.
|
||||
if (success && version >= 2 && numBlocks > 0) {
|
||||
|
||||
+9
-1
@@ -11,7 +11,15 @@
|
||||
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
|
||||
|
||||
// UTXO snapshot format version
|
||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 2;
|
||||
// v1: original (headers + UTXOs)
|
||||
// v2: + embeds raw blk0001.dat for full self-contained bootstrap
|
||||
// v3: + carries setStakeSeen entries (prevoutStake, nStakeTime) so a
|
||||
// snapshot-loaded node has the recent PoS stake-collision set restored
|
||||
// immediately, without needing to walk the last N blocks on startup.
|
||||
// Required for the anti-spam "too little proof-of-stake" check in
|
||||
// ProcessBlock to work correctly post-snapshot-bootstrap, since
|
||||
// LoadBlockIndex only walks 500 blocks back from pindexBest.
|
||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 3;
|
||||
|
||||
// Number of block index entries to include in snapshot (covers difficulty,
|
||||
// median time, stake modifier, and reorg depth requirements)
|
||||
|
||||
Reference in New Issue
Block a user