Files
triangles_v5/src/miner.cpp
T
Krystie 935d1d527c fix(consensus): remove local-finality, fix getheaders fork recovery
fix/consensus-convergence — the rules around reorg finality and the
getheaders fork-peer handler previously used locally advanced state
that prevented two honest nodes from converging after extended
disconnection. This commit removes the local-finality rules and
restores convergence above the last globally shared hardened
checkpoint.

Reorganize() now:
- Rejects reorgs whose fork point is at or below the compiled
  hardened checkpoint (sourced from Checkpoints::GetLastCheckpoint
  at startup, never advanced at runtime).
- Above the checkpoint: greatest cumulative chain trust wins. No
  depth cap, no local finality, no 10% trust hysteresis.

pindexFinalized is renamed to pindexLastHardenedCheckpoint to make
clear that the variable now refers to the compiled checkpoint anchor,
not a locally advanced finality depth. Its initialization in init.cpp
runs once at startup; no runtime advancement.

The getheaders handler now serves canonical history based on what the
peer actually knows:
- If the peer's locator contains the hardened checkpoint, serve
  headers from the checkpoint forward.
- Otherwise, serve from the last common ancestor (falling back to
  genesis if no overlap exists). This lets a forked peer recover
  instead of being handed a header whose parent it doesn't have.

CBlockLocator gains two small public accessors (Has, FindCommonAncestorInMainChain)
so the recovery code doesn't have to reach into protected state.

Staking safety gate is now continuous in StakeMiner (main.cpp's
IsStakingSafe runs every iteration). Removed the once-only fTryToSync
flag whose reset-after-first-use made the strong peer-count / IBD
check ineffective after a network outage mid-staking. The gate refuses
to stake when IBD is active, fewer than 2 handshaken peers exist, our
height is behind the peer median, or a peer reports a tip >=2 blocks
ahead of ours (possible competing fork signal).

Tests:
- consensus_safety_tests.cpp: 6 new tests pinning the convergence
  rule's structure against src/main.cpp and src/init.cpp. Replaces the
  old max_reorg_depth_enforced test (which pinned the now-removed
  local-finality constant).
- staking_tests.cpp: 3 new tests pinning the continuous gate's
  behavior and the absence of fTryToSync from runtime code.

All 277 unit-test cases (21,752 assertions) pass locally. The Qt GUI
was not rebuilt; the daemon (trianglesd), CLI (triangles-cli), and
test binary (test_triangles) all link and execute.

Reviewed-against: pre-commit HEAD
No push to master performed per standing rule.
2026-07-16 21:47:21 -07:00

489 lines
17 KiB
C++

// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2012 The Bitcoin developers
// Copyright (c) 2014-2015 Triangles team
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "txdb.h"
#include "miner.h"
#include "kernel.h"
using namespace std;
//////////////////////////////////////////////////////////////////////////////
//
// TrianglesMiner
//
//
// PoW mining helpers (SHA256Transform, FormatHashBlocks, FormatHashBuffers,
// IncrementExtraNonce, CheckWork) removed - PoW ended at block 9000.
// Some explaining would be appreciated
class COrphan
{
public:
CTransaction* ptx;
set<uint256> setDependsOn;
double dPriority;
double dFeePerKb;
COrphan(CTransaction* ptxIn)
{
ptx = ptxIn;
dPriority = dFeePerKb = 0;
}
void print() const
{
printf("COrphan(hash=%s, dPriority=%.1f, dFeePerKb=%.1f)\n",
ptx->GetHash().ToString().substr(0,10).c_str(), dPriority, dFeePerKb);
for (uint256 hash : setDependsOn)
printf(" setDependsOn %s\n", hash.ToString().substr(0,10).c_str());
}
};
uint64_t nLastBlockTx = 0;
uint64_t nLastBlockSize = 0;
int64_t nLastCoinStakeSearchInterval = 0;
// We want to sort transactions by priority and fee, so:
using TxPriority = std::tuple<double, double, CTransaction*>;
class TxPriorityCompare
{
bool byFee;
public:
TxPriorityCompare(bool _byFee) : byFee(_byFee) { }
bool operator()(const TxPriority& a, const TxPriority& b)
{
// #8: Fee-weighted priority for PoS staking.
// When sorting by fee (PoS mode), apply a 2x weight to fees so
// higher-fee transactions are prioritized over coin-age-only ones.
// This maximizes staking rewards for the minter.
if (byFee)
{
double feeA = std::get<1>(a) * 2.0; // fee boost
double feeB = std::get<1>(b) * 2.0;
if (feeA == feeB)
return std::get<0>(a) < std::get<0>(b);
return feeA < feeB;
}
else
{
if (std::get<0>(a) == std::get<0>(b))
return std::get<1>(a) < std::get<1>(b);
return std::get<0>(a) < std::get<0>(b);
}
}
};
// CreateNewBlock: fProofOfStake: try (best effort) to make a proof-of-stake block
CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
{
// Create new block
unique_ptr<CBlock> pblock(new CBlock());
if (!pblock.get())
return nullptr;
CBlockIndex* pindexPrev = pindexBest;
// Create coinbase tx
CTransaction txNew;
txNew.vin.resize(1);
txNew.vin[0].prevout.SetNull();
txNew.vout.resize(1);
if (!fProofOfStake)
{
CReserveKey reservekey(pwallet);
txNew.vout[0].scriptPubKey.SetDestination(reservekey.GetReservedKey().GetID());
}
else
{
// Height first in coinbase required for block.version=2
txNew.vin[0].scriptSig = (CScript() << pindexPrev->nHeight+1) + COINBASE_FLAGS;
assert(txNew.vin[0].scriptSig.size() <= 100);
txNew.vout[0].SetEmpty();
}
// Add our coinbase tx as first transaction
pblock->vtx.push_back(txNew);
// Largest block you're willing to create:
unsigned int nBlockMaxSize = GetArg("-blockmaxsize", MAX_BLOCK_SIZE_GEN/2);
// Limit to betweeen 1K and MAX_BLOCK_SIZE-1K for sanity:
nBlockMaxSize = std::max((unsigned int)1000, std::min((unsigned int)(MAX_BLOCK_SIZE-1000), nBlockMaxSize));
// How much of the block should be dedicated to high-priority transactions,
// included regardless of the fees they pay
unsigned int nBlockPrioritySize = GetArg("-blockprioritysize", 27000);
nBlockPrioritySize = std::min(nBlockMaxSize, nBlockPrioritySize);
// Minimum block size you want to create; block will be filled with free transactions
// until there are no more or the block reaches this size:
unsigned int nBlockMinSize = GetArg("-blockminsize", 0);
nBlockMinSize = std::min(nBlockMaxSize, nBlockMinSize);
// Fee-per-kilobyte amount considered the same as "free"
// Be careful setting this: if you set it to zero then
// a transaction spammer can cheaply fill blocks using
// 1-satoshi-fee transactions. It should be set above the real
// cost to you of processing a transaction.
int64_t nMinTxFee = MIN_TX_FEE;
if (mapArgs.count("-mintxfee"))
ParseMoney(mapArgs["-mintxfee"], nMinTxFee);
pblock->nBits = GetNextTargetRequired(pindexPrev, fProofOfStake);
// Collect memory pool transactions into the block
int64_t nFees = 0;
{
LOCK2(cs_main, mempool.cs);
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
// Priority order to process transactions
list<COrphan> vOrphan; // list memory doesn't move
map<uint256, vector<COrphan*> > mapDependers;
// This vector will be sorted into a priority queue:
vector<TxPriority> vecPriority;
vecPriority.reserve(mempool.mapTx.size());
for (auto& [hash, tx] : mempool.mapTx)
{
if (tx.IsCoinBase() || tx.IsCoinStake() || !tx.IsFinal())
continue;
COrphan* porphan = nullptr;
double dPriority = 0;
int64_t nTotalIn = 0;
bool fMissingInputs = false;
for (const CTxIn& txin : tx.vin)
{
// Read prev transaction
CTransaction txPrev;
CTxIndex txindex;
if (!txPrev.ReadFromDisk(txdb, txin.prevout, txindex))
{
// This should never happen; all transactions in the memory
// pool should connect to either transactions in the chain
// or other transactions in the memory pool.
if (!mempool.mapTx.count(txin.prevout.hash))
{
printf("ERROR: mempool transaction missing input\n");
if (fDebug) assert("mempool transaction missing input" == 0);
fMissingInputs = true;
if (porphan)
vOrphan.pop_back();
break;
}
// Has to wait for dependencies
if (!porphan)
{
// Use list for automatic deletion
vOrphan.push_back(COrphan(&tx));
porphan = &vOrphan.back();
}
mapDependers[txin.prevout.hash].push_back(porphan);
porphan->setDependsOn.insert(txin.prevout.hash);
nTotalIn += mempool.mapTx[txin.prevout.hash].vout[txin.prevout.n].nValue;
continue;
}
int64_t nValueIn = txPrev.vout[txin.prevout.n].nValue;
nTotalIn += nValueIn;
int nConf = txindex.GetDepthInMainChain();
dPriority += (double)nValueIn * nConf;
}
if (fMissingInputs) continue;
// Priority is sum(valuein * age) / txsize
unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK, PROTOCOL_VERSION);
dPriority /= nTxSize;
// This is a more accurate fee-per-kilobyte than is used by the client code, because the
// client code rounds up the size to the nearest 1K. That's good, because it gives an
// incentive to create smaller transactions.
double dFeePerKb = double(nTotalIn-tx.GetValueOut()) / (double(nTxSize)/1000.0);
if (porphan)
{
porphan->dPriority = dPriority;
porphan->dFeePerKb = dFeePerKb;
}
else
vecPriority.push_back(TxPriority(dPriority, dFeePerKb, &tx));
}
// Collect transactions into block
uint64_t nBlockSize = 1000;
uint64_t nBlockTx = 0;
int nBlockSigOps = 100;
bool fSortedByFee = (nBlockPrioritySize <= 0);
TxPriorityCompare comparer(fSortedByFee);
std::make_heap(vecPriority.begin(), vecPriority.end(), comparer);
while (!vecPriority.empty())
{
// Take highest priority transaction off the priority queue:
double dPriority = std::get<0>(vecPriority.front());
double dFeePerKb = std::get<1>(vecPriority.front());
CTransaction& tx = *(std::get<2>(vecPriority.front()));
std::pop_heap(vecPriority.begin(), vecPriority.end(), comparer);
vecPriority.pop_back();
// Size limits
unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK, PROTOCOL_VERSION);
if (nBlockSize + nTxSize >= nBlockMaxSize)
continue;
// Legacy limits on sigOps:
unsigned int nTxSigOps = tx.GetLegacySigOpCount();
if (nBlockSigOps + nTxSigOps >= MAX_BLOCK_SIGOPS)
continue;
// Timestamp limit
if (tx.nTime > GetAdjustedTime() || (fProofOfStake && tx.nTime > pblock->vtx[0].nTime))
continue;
// Transaction fee
int64_t nMinFee = tx.GetMinFee(nBlockSize, GetMinFeeMode::Block);
// Skip free transactions if we're past the minimum block size:
if (fSortedByFee && (dFeePerKb < nMinTxFee) && (nBlockSize + nTxSize >= nBlockMinSize))
continue;
// Prioritize by fee once past the priority size or we run out of high-priority
// transactions:
if (!fSortedByFee &&
((nBlockSize + nTxSize >= nBlockPrioritySize) || (dPriority < COIN * 144 / 250)))
{
fSortedByFee = true;
comparer = TxPriorityCompare(fSortedByFee);
std::make_heap(vecPriority.begin(), vecPriority.end(), comparer);
}
// Connecting shouldn't fail due to dependency on other memory pool transactions
// because we're already processing them in order of dependency
MapPrevTx mapInputs;
MapPrevTx mapEmpty;
bool fInvalid;
if (!tx.FetchInputs(txdb, mapEmpty, false, true, mapInputs, fInvalid))
continue;
int64_t nTxFees = tx.GetValueIn(mapInputs)-tx.GetValueOut();
if (nTxFees < nMinFee)
continue;
nTxSigOps += tx.GetP2SHSigOpCount(mapInputs);
if (nBlockSigOps + nTxSigOps >= MAX_BLOCK_SIGOPS)
continue;
if (!tx.ConnectInputs(txdb, mapInputs, pindexPrev, false, true))
continue;
// Added
pblock->vtx.push_back(tx);
nBlockSize += nTxSize;
++nBlockTx;
nBlockSigOps += nTxSigOps;
nFees += nTxFees;
if (fDebug && GetBoolArg("-printpriority"))
{
printf("priority %.1f feeperkb %.1f txid %s\n",
dPriority, dFeePerKb, tx.GetHash().ToString().c_str());
}
// Add transactions that depend on this one to the priority queue
uint256 hash = tx.GetHash();
if (mapDependers.count(hash))
{
for (COrphan* porphan : mapDependers[hash])
{
if (!porphan->setDependsOn.empty())
{
porphan->setDependsOn.erase(hash);
if (porphan->setDependsOn.empty())
{
vecPriority.push_back(TxPriority(porphan->dPriority, porphan->dFeePerKb, porphan->ptx));
std::push_heap(vecPriority.begin(), vecPriority.end(), comparer);
}
}
}
}
}
nLastBlockTx = nBlockTx;
nLastBlockSize = nBlockSize;
if (fDebug && GetBoolArg("-printpriority"))
printf("CreateNewBlock(): total size %" PRIu64 "\n", nBlockSize);
if (!fProofOfStake)
pblock->vtx[0].vout[0].nValue = GetProofOfWorkReward(nFees);
if (pFees)
*pFees = nFees;
// Fill in header
pblock->hashPrevBlock = pindexPrev->GetBlockHash();
pblock->nTime = max(pindexPrev->GetPastTimeLimit()+1, pblock->GetMaxTransactionTime());
pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime(), pindexPrev->nHeight + 1));
if (!fProofOfStake)
pblock->UpdateTime(pindexPrev);
pblock->nNonce = 0;
}
return pblock.release();
}
bool CheckStake(CBlock* pblock, CWallet& wallet)
{
uint256 proofHash = 0, hashTarget = 0;
uint256 hash = pblock->GetHash();
if(!pblock->IsProofOfStake())
return error("CheckStake() : %s is not a proof-of-stake block", hash.GetHex().c_str());
if (pblock->vtx.size() < 2)
return error("CheckStake() : block has no coinstake transaction");
// verify hash target and signature of coinstake tx
if (!CheckProofOfStake(pblock->vtx[1], pblock->nBits, proofHash, hashTarget))
return error("CheckStake() : proof-of-stake checking failed");
//// debug print
printf("CheckStake() : new proof-of-stake block found \n hash: %s \nproofhash: %s \ntarget: %s\n", hash.GetHex().c_str(), proofHash.GetHex().c_str(), hashTarget.GetHex().c_str());
pblock->print();
printf("out %s\n", FormatMoney(pblock->vtx[1].GetValueOut()).c_str());
// Found a solution
{
LOCK(cs_main);
if (pblock->hashPrevBlock != hashBestChain)
return error("CheckStake() : generated block is stale");
// Track how many getdata requests this block gets
{
LOCK(wallet.cs_wallet);
wallet.mapRequestCount[hash] = 0;
}
// Process this block the same as if we had received it from another node
if (!ProcessBlock(nullptr, pblock))
return error("CheckStake() : ProcessBlock, block not accepted");
}
return true;
}
void StakeMiner(CWallet *pwallet)
{
SetThreadPriority(THREAD_PRIORITY_LOWEST);
// Make this thread recognisable as the mining thread
RenameThread("Triangles-miner");
bool fForceStaking = GetBoolArg("-forcestaking", false);
while (true)
{
if (fShutdown)
return;
while (pwallet->IsLocked())
{
nLastCoinStakeSearchInterval = 0;
MilliSleep(1000);
if (fShutdown)
return;
}
// Continuous staking safety gate (fix/consensus-convergence).
//
// Pre-fix: a one-shot strong check ran only once after the inner
// wait exited. Losing peers mid-staking left the staker running
// on a potentially isolated chain. This gate is evaluated on
// EVERY staking attempt.
//
// Refuses to stake when:
// - IBD is active (IsInitialBlockDownload)
// - fewer than 2 fully handshaken non-disconnecting peers
// - our height is behind the peer median
// - a known competing valid fork is at or above our active chain trust
//
// `-forcestaking` remains an explicit operator override (with the
// same warning as before) for stall recovery.
if (!fForceStaking)
{
if (!IsStakingSafe(pwallet, vNodes))
{
nLastCoinStakeSearchInterval = 0;
MilliSleep(1000);
continue;
}
}
else if (vNodes.empty() || IsInitialBlockDownload())
{
// Force path still requires wallet connectivity; the rest of
// the gate is the operator's responsibility.
nLastCoinStakeSearchInterval = 0;
MilliSleep(1000);
continue;
}
//
// Update cached stake weight for UI display (avoids heavy work on UI thread)
//
{
uint64_t nMinWeight = 0, nMaxWeight = 0, nWeight = 0;
pwallet->GetStakeWeight(*pwallet, nMinWeight, nMaxWeight, nWeight);
pwallet->nCachedStakeWeight = nWeight;
pwallet->nCachedStakeWeightTime = GetTime();
}
//
// Create new block
//
int64_t nFees;
unique_ptr<CBlock> pblock(CreateNewBlock(pwallet, true, &nFees));
if (!pblock.get())
{
MilliSleep(5000);
continue;
}
// Try to sign the block
if (pblock->SignBlock(*pwallet, nFees))
{
printf("StakeMiner(): A proof-of-stake block has been found! %s\n", pblock->GetHash().ToString().c_str());
SetThreadPriority(THREAD_PRIORITY_NORMAL);
bool fAccepted = CheckStake(pblock.get(), *pwallet);
SetThreadPriority(THREAD_PRIORITY_LOWEST);
if (fAccepted)
{
MilliSleep(500);
}
else
{
// Block was orphaned or rejected — apply a cooldown to reduce
// fork oscillation. Without this, the staker immediately retries
// with a different timestamp, potentially creating competing forks.
printf("StakeMiner(): block not accepted, cooldown 30s\n");
MilliSleep(30000);
}
}
else
MilliSleep(500);
}
}