From 1f3deacb7a619e5e44bdc8f86f208b2e1d953a98 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Sat, 28 Mar 2026 00:59:05 -0700 Subject: [PATCH] Fix persistent PoS chain forks with deterministic tiebreaker and tighter timestamps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PoS blocks at the same height have identical difficulty, producing equal chain trust scores. The old "strictly greater" comparison meant first-seen-wins, causing permanent forks when nodes received competing blocks in different order. v5.4 fork (block 2186941) adds: - Deterministic tiebreaker: equal-trust chains resolve to the lower tip hash - Tighter time drift: ±3 min (was ±10 min), reducing the competing block window Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 16 +++++++++++++--- src/main.h | 8 ++++++-- src/miner.cpp | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8fb9ce7..856a637 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2568,7 +2568,17 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u // New best — keep the batch open so SetBestChain can add ConnectBlock // writes to the same transaction, cutting the per-block commit count in half. + // v5.4: deterministic tiebreaker — when two chains have equal trust, + // all nodes agree on the one whose tip has the lower block hash. + // This prevents permanent forks from PoS blocks with identical difficulty. + bool fNewBest = false; if (pindexNew->nChainTrust > nBestChainTrust) + fNewBest = true; + else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest && + pindexNew->GetBlockHash() < pindexBest->GetBlockHash()) + fNewBest = true; + + if (fNewBest) { if (!SetBestChain(txdb, pindexNew)) return false; @@ -2719,7 +2729,7 @@ bool CBlock::AcceptBlock() if (nHeight % 10000 == 0 || nHeight > 2186900) printf("ProcessBlock(): Check proof-of-stake/work OK for block %d\n", nHeight); // Check timestamp against prev - if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime()) < pindexPrev->GetBlockTime()) + if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime(), nHeight) < pindexPrev->GetBlockTime()) return error("AcceptBlock() : block's timestamp is too early"); // Check that all transactions are finalized @@ -2999,13 +3009,13 @@ bool CBlock::SignBlock(CWallet& wallet, int64_t nFees) { if (wallet.CreateCoinStake(wallet, nBits, nSearchTime-nLastCoinStakeSearchTime, nFees, txCoinStake, key)) { - if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime()))) + if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1))) { // make sure coinstake would meet timestamp protocol // as it would be the same as the block timestamp vtx[0].nTime = nTime = txCoinStake.nTime; nTime = max(pindexBest->GetPastTimeLimit()+1, GetMaxTransactionTime()); - nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime())); + nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1)); // we have to make sure that we have no future timestamps in // our transactions set diff --git a/src/main.h b/src/main.h index 7cd09fa..86659fd 100644 --- a/src/main.h +++ b/src/main.h @@ -29,6 +29,7 @@ class CNode; static const int CUTOFF_POW_BLOCK = 9000; static const int CRAPCHAIN_CUTOFF_BLOCK = 17691; // pre-Pharao (version 4) blockchain until block 17691 static const int FORK_HEIGHT_V5 = 17651; // v5 hard fork: decentralization + Tor v3 (next block after last checkpoint) +static const int FORK_HEIGHT_V5_4 = 2186941; // v5.4: tighter timestamps, deterministic fork resolution static const unsigned int MAX_BLOCK_SIZE = 1000000; static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE/2; @@ -56,8 +57,11 @@ static const int fHaveUPnP = false; static const uint256 hashGenesisBlockOfficial("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); static const uint256 hashGenesisBlockTestNet ("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); -inline int64_t PastDrift(int64_t nTime) { return nTime - 10 * 60; } // up to 10 minutes from the past -inline int64_t FutureDrift(int64_t nTime) { return nTime + 10 * 60; } // up to 10 minutes from the future +inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 3 * 60 : 10 * 60; } +inline int64_t PastDrift(int64_t nTime, int nHeight) { return nTime - GetMaxTimeDrift(nHeight); } +inline int64_t FutureDrift(int64_t nTime, int nHeight) { return nTime + GetMaxTimeDrift(nHeight); } +inline int64_t PastDrift(int64_t nTime) { return PastDrift(nTime, nBestHeight); } +inline int64_t FutureDrift(int64_t nTime) { return FutureDrift(nTime, nBestHeight); } extern CScript COINBASE_FLAGS; diff --git a/src/miner.cpp b/src/miner.cpp index 4654475..c17cb2a 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -365,7 +365,7 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees) // Fill in header pblock->hashPrevBlock = pindexPrev->GetBlockHash(); pblock->nTime = max(pindexPrev->GetPastTimeLimit()+1, pblock->GetMaxTransactionTime()); - pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime())); + pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime(), pindexPrev->nHeight + 1)); if (!fProofOfStake) pblock->UpdateTime(pindexPrev); pblock->nNonce = 0;