6b2293ad1a
FastImportBlockFile() was calling GetStakeModifierChecksum(pindexNew) which calls GetBlockHash() which dereferences *phashBlock — but phashBlock was still null because the mapBlockIndex.insert that sets it happened 10 lines later. This caused a segfault (exit 139) on every fresh -reindex with no existing chainstate. Fix: move the mapBlockIndex.insert + phashBlock assignment before the GetStakeModifierChecksum call. Pure ordering fix, no logic change.
6383 lines
242 KiB
C++
6383 lines
242 KiB
C++
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
|
// Copyright (c) 2009-2012 The Bitcoin developers
|
|
// Distributed under the MIT/X11 software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include "checkpoints.h"
|
|
#include "db.h"
|
|
|
|
#include <cmath>
|
|
#include "txdb.h"
|
|
#include "net.h"
|
|
#include "init.h"
|
|
#include "ui_interface.h"
|
|
#include "kernel.h"
|
|
#include "smessage.h"
|
|
#include "tor/onion_v3.h"
|
|
#include "tor/tor_embedded.h"
|
|
#ifdef ENABLE_ZMQ
|
|
#include "zmqpublishnotifier.h"
|
|
#endif
|
|
#include "notificationqueue.h"
|
|
#include "addressindex.h"
|
|
#include "snapshotnet.h"
|
|
#include "syncmanager.h"
|
|
#include <algorithm>
|
|
#include <deque>
|
|
#include <memory>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
|
|
|
|
using namespace std;
|
|
using namespace boost;
|
|
namespace fs = std::filesystem;
|
|
|
|
//
|
|
// Global state
|
|
//
|
|
|
|
CCriticalSection cs_setpwalletRegistered;
|
|
set<CWallet*> setpwalletRegistered;
|
|
|
|
CCriticalSection cs_main;
|
|
|
|
CTxMemPool mempool;
|
|
unsigned int nTransactionsUpdated = 0;
|
|
std::unique_ptr<CCheckQueue<CScriptCheck>> pScriptCheckQueue;
|
|
|
|
map<uint256, CBlockIndex*> mapBlockIndex;
|
|
set<pair<COutPoint, unsigned int> > setStakeSeen;
|
|
|
|
|
|
static CBigNum bnProofOfWorkLimit(~uint256(0) >> 8);
|
|
static CBigNum bnProofOfStakeLimit(~uint256(0) >> 8);
|
|
static CBigNum bnProofOfWorkLimitTestNet(~uint256(0) >> 8);
|
|
static CBigNum bnProofOfStakeLimitTestNet(~uint256(0) >> 8);
|
|
|
|
unsigned int nTargetSpacing = 60 * 2; // 2 minutes
|
|
unsigned int nStakeMinAge = 60 * 60 * 1; // 1 hour
|
|
unsigned int nStakeMaxAge = 60 * 60 * 12; //12 hours
|
|
unsigned int nModifierInterval = 5 * 60 ; // .5 time to elapse before new modifier is computed
|
|
|
|
int64_t nChainStartTime = 1405500418;
|
|
int nCoinbaseMaturity = 7; //overall maturity: currently 7 blocks, maybe subject to increase
|
|
|
|
CBlockIndex* pindexGenesisBlock = nullptr;
|
|
int nBestHeight = -1;
|
|
bool fLoadedFromSnapshot = false; // set true by UtxoSnapshot::LoadSnapshot on success
|
|
int nHighestInvWalk = 0; // height of walk-forward progress through already-have inv
|
|
uint256 hashHighestInvWalk = 0; // hash of that block
|
|
|
|
uint256 nBestChainTrust = 0;
|
|
uint256 nBestInvalidTrust = 0;
|
|
|
|
uint256 hashBestChain = 0;
|
|
CBlockIndex* pindexBest = nullptr;
|
|
CBlockIndex* pindexLastHardenedCheckpoint = nullptr; // last compiled hardened checkpoint in our local index (set at startup only; never advanced at runtime)
|
|
|
|
// nAssumeValidThreshold: highest block height covered by the assumeValid
|
|
// fast path. The fast path skips sigops/script/UTXO validation for blocks
|
|
// at or below this height (we've already verified the chain up to here).
|
|
// Initially 0 (only hardcoded checkpoints trigger fast path). Advances by
|
|
// ASSUME_VALID_BUFFER blocks BEHIND the tip after each successful SetBestChain.
|
|
// Persisted via wallet DB so a restart doesn't re-validate 2.2M blocks.
|
|
int nAssumeValidThreshold = 0;
|
|
bool fAddressIndex = false;
|
|
int64_t nTimeBestReceived = 0;
|
|
|
|
// ─── Fork detection (#6) ────────────────────────────────────────────────────
|
|
// Background monitor that compares our chain tip against peer medians.
|
|
// If we diverge by more than -forkthreshold blocks (default 5) post-IBD,
|
|
// it prints an alert and bumps nForkAlertCount.
|
|
int nForkAlertCount = 0;
|
|
static int nLastForkCheckHeight = 0;
|
|
|
|
void ThreadForkDetector(void*)
|
|
{
|
|
RenameThread("Triangles-fork-detector");
|
|
printf("Fork detector: started (checks every 60s post-IBD)\n");
|
|
while (!fShutdown)
|
|
{
|
|
MilliSleep(60000); // check every 60s
|
|
if (fShutdown) break;
|
|
if (IsInitialBlockDownload()) continue;
|
|
|
|
int nPeerMedian = GetNumBlocksOfPeers();
|
|
int nOurHeight = nBestHeight;
|
|
int lag = nPeerMedian - nOurHeight;
|
|
|
|
int threshold = GetArg("-forkthreshold", 5);
|
|
if (threshold < 1) threshold = 1;
|
|
|
|
if (lag >= threshold && nOurHeight > 0)
|
|
{
|
|
nForkAlertCount++;
|
|
printf("*** FORK ALERT #%d: local height %d is %d blocks behind peer median %d ***\n",
|
|
nForkAlertCount, nOurHeight, lag, nPeerMedian);
|
|
printf("*** Possible fork or sync stall. Check peers: 'getpeerinfo' and chain: 'getblockhash %d' ***\n",
|
|
nOurHeight);
|
|
|
|
// Severe lag is diagnostic only. Recovery must be explicitly
|
|
// initiated by an operator after backups; never request an
|
|
// automatic shutdown that could lead to chain-state deletion.
|
|
if (lag >= threshold * 3 && GetBoolArg("-autorerebuild", false))
|
|
{
|
|
printf("*** FORK DETECTOR: automatic rebuild is disabled; operator review required ***\n");
|
|
}
|
|
}
|
|
|
|
// Also check for hash divergence: if we have the same height as
|
|
// peers but different block hash, that's a definite fork
|
|
if (lag == 0 && nOurHeight != nLastForkCheckHeight && nOurHeight > 0)
|
|
{
|
|
nLastForkCheckHeight = nOurHeight;
|
|
// Log our chain tip hash for comparison
|
|
if (fDebug)
|
|
printf("Fork detector: height %d hash %s (peer median matches)\n",
|
|
nOurHeight, hashBestChain.ToString().substr(0, 16).c_str());
|
|
}
|
|
}
|
|
printf("Fork detector: stopped\n");
|
|
}
|
|
|
|
CMedianFilter<int> cPeerBlockCounts(5, 0); // Amount of blocks that other nodes claim to have
|
|
|
|
CScriptVerifyCache scriptVerifyCache;
|
|
|
|
map<uint256, std::unique_ptr<CBlock>> mapOrphanBlocks;
|
|
multimap<uint256, CBlock*> mapOrphanBlocksByPrev;
|
|
set<pair<COutPoint, unsigned int> > setStakeSeenOrphan;
|
|
|
|
|
|
map<uint256, CTransaction> mapOrphanTransactions;
|
|
map<uint256, set<uint256> > mapOrphanTransactionsByPrev;
|
|
|
|
// Compact block relay: partial blocks awaiting missing transactions
|
|
struct CPartialBlock
|
|
{
|
|
CCompactBlock cmpctblock;
|
|
std::vector<CTransaction> vTxFilled; // filled transactions (indexed by position)
|
|
std::set<uint16_t> setMissing; // indices still needed
|
|
int64_t nReceiveTime;
|
|
CNode* pfrom;
|
|
};
|
|
static std::map<uint256, CPartialBlock> mapPartialBlocks;
|
|
static const unsigned int MAX_PARTIAL_BLOCKS = 5;
|
|
static const int64_t PARTIAL_BLOCK_TTL = 30; // seconds
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BIP152 Compact Block helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** SipHash-2-4 primitive.
|
|
*
|
|
* Implements the SipHash-2-4 PRF used by BIP152 for short transaction IDs.
|
|
* Produces a 64-bit hash from a 128-bit key and variable-length input.
|
|
*/
|
|
static inline uint64_t SipHash(uint64_t k0, uint64_t k1, const unsigned char* data, size_t size)
|
|
{
|
|
uint64_t v0 = 0x736f6d6570736575ULL ^ k0;
|
|
uint64_t v1 = 0x646f72616e646f6dULL ^ k1;
|
|
uint64_t v2 = 0x6c7967656e657261ULL ^ k0;
|
|
uint64_t v3 = 0x7465646279746573ULL ^ k1;
|
|
|
|
auto rotl = [](uint64_t x, int b) { return (x << b) | (x >> (64 - b)); };
|
|
|
|
// Process 8-byte blocks
|
|
const unsigned char* end = data + size - (size % 8);
|
|
while (data < end)
|
|
{
|
|
uint64_t m;
|
|
memcpy(&m, data, 8);
|
|
v3 ^= m;
|
|
// SipHash-2: 2 rounds
|
|
v0 += v1; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32);
|
|
v2 += v3; v3 = rotl(v3, 16); v3 ^= v2;
|
|
v0 += v3; v3 = rotl(v3, 21); v3 ^= v0;
|
|
v2 += v1; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32);
|
|
v0 += v1; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32);
|
|
v2 += v3; v3 = rotl(v3, 16); v3 ^= v2;
|
|
v0 += v3; v3 = rotl(v3, 21); v3 ^= v0;
|
|
v2 += v1; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32);
|
|
v0 ^= m;
|
|
data += 8;
|
|
}
|
|
|
|
// Final block (0-7 bytes + length byte)
|
|
unsigned char pad[8] = {0};
|
|
memcpy(pad, data, size % 8);
|
|
pad[7] = (unsigned char)size;
|
|
uint64_t m;
|
|
memcpy(&m, pad, 8);
|
|
v3 ^= m;
|
|
v0 += v1; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32);
|
|
v2 += v3; v3 = rotl(v3, 16); v3 ^= v2;
|
|
v0 += v3; v3 = rotl(v3, 21); v3 ^= v0;
|
|
v2 += v1; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32);
|
|
v0 += v1; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32);
|
|
v2 += v3; v3 = rotl(v3, 16); v3 ^= v2;
|
|
v0 += v3; v3 = rotl(v3, 21); v3 ^= v0;
|
|
v2 += v1; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32);
|
|
v0 ^= m;
|
|
|
|
// Finalization: 4 rounds + XOR fold
|
|
v2 ^= 0xff;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
v0 += v1; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32);
|
|
v2 += v3; v3 = rotl(v3, 16); v3 ^= v2;
|
|
v0 += v3; v3 = rotl(v3, 21); v3 ^= v0;
|
|
v2 += v1; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32);
|
|
}
|
|
return v0 ^ v1 ^ v2 ^ v3;
|
|
}
|
|
|
|
/** Compute a BIP152-style 48-bit short transaction ID.
|
|
*
|
|
* Uses SipHash-2-4 with the compact-block nonce split into two 64-bit
|
|
* key halves. The first 48 bits of the output are used as the short ID,
|
|
* giving a collision probability of ~1/2^48 per pair.
|
|
*/
|
|
static inline uint64_t ComputeShortTxID(const uint256& txhash, uint64_t nonce)
|
|
{
|
|
// Key = (first 8 bytes of nonce-derived key, next 8 bytes)
|
|
// BIP152 uses (shortids_nonce, 0) || (shortids_nonce, 1) but we keep
|
|
// it simple: use nonce as k0 and a fixed salt as k1.
|
|
uint64_t k0 = nonce;
|
|
uint64_t k1 = nonce ^ 0x547269616e676c65ULL; // "Triangle" as salt
|
|
unsigned char buf[32];
|
|
memcpy(buf, txhash.begin(), 32);
|
|
uint64_t hash = SipHash(k0, k1, buf, 32);
|
|
return hash & 0xFFFFFFFFFFFFULL; // truncate to 48 bits
|
|
}
|
|
|
|
/** Send a compact block to a single peer (BIP152).
|
|
*
|
|
* Serializes the block header + nonce + short IDs + prefilled transactions.
|
|
* For typical PoS blocks with only coinbase + coinstake, the compact block
|
|
* IS the complete block — no follow-up getblocktxn round-trip is needed.
|
|
*/
|
|
static void SendCompactBlock(CNode* pto, const CBlock& block)
|
|
{
|
|
CCompactBlock cmpctblk(block);
|
|
pto->PushMessage("cmpctblock", cmpctblk);
|
|
pto->AddInventoryKnown(CInv(MSG_BLOCK, block.GetHash()));
|
|
}
|
|
|
|
/** Process a received compact block (BIP152).
|
|
*
|
|
* Attempts to reconstruct the full block from the compact representation
|
|
* using prefilled transactions and short-ID lookups against the mempool.
|
|
* On success, calls ProcessBlock. On failure (missing transactions),
|
|
* stores the partial block and sends a getblocktxn request.
|
|
*
|
|
* Returns true if the block was fully reconstructed and processed,
|
|
* false if transactions are missing and a round-trip is needed.
|
|
*/
|
|
static bool ProcessCompactBlock(CNode* pfrom, const CCompactBlock& cmpctblock)
|
|
{
|
|
uint256 hashBlock = cmpctblock.GetBlockHash();
|
|
CInv inv(MSG_BLOCK, hashBlock);
|
|
pfrom->AddInventoryKnown(inv);
|
|
|
|
// Skip if we already have this block
|
|
if (mapBlockIndex.count(hashBlock))
|
|
return true;
|
|
|
|
// Reconstruct the block header
|
|
CBlock block;
|
|
block.nVersion = cmpctblock.nVersion;
|
|
block.hashPrevBlock = cmpctblock.hashPrevBlock;
|
|
block.hashMerkleRoot = cmpctblock.hashMerkleRoot;
|
|
block.nTime = cmpctblock.nTime;
|
|
block.nBits = cmpctblock.nBits;
|
|
block.nNonce = cmpctblock.nNonce;
|
|
block.vchBlockSig = cmpctblock.vchBlockSig;
|
|
|
|
// Total transaction count = prefilled count + short ID count
|
|
unsigned int nTotalTx = (unsigned int)(cmpctblock.vPrefilledTxn.size() + cmpctblock.vShortTxIds.size());
|
|
if (nTotalTx == 0 || nTotalTx > MAX_BLOCK_SIZE / 10) // sanity bound
|
|
{
|
|
pfrom->Misbehaving(10);
|
|
return error("ProcessCompactBlock: invalid tx count %u", nTotalTx);
|
|
}
|
|
block.vtx.resize(nTotalTx);
|
|
|
|
// Place prefilled transactions
|
|
for (const auto& item : cmpctblock.vPrefilledTxn)
|
|
{
|
|
if (item.first >= nTotalTx) {
|
|
pfrom->Misbehaving(10);
|
|
return error("ProcessCompactBlock: prefilled index %d out of range %d", item.first, nTotalTx);
|
|
}
|
|
block.vtx[item.first] = item.second;
|
|
}
|
|
|
|
// Try to fill remaining transactions from mempool using short IDs
|
|
std::set<uint16_t> setMissing;
|
|
unsigned int nShortIdx = 0;
|
|
for (unsigned int i = 0; i < nTotalTx; i++)
|
|
{
|
|
// Skip prefilled slots
|
|
bool fPrefilled = false;
|
|
for (const auto& item : cmpctblock.vPrefilledTxn) {
|
|
if (item.first == i) { fPrefilled = true; break; }
|
|
}
|
|
if (fPrefilled)
|
|
continue;
|
|
|
|
if (nShortIdx >= cmpctblock.vShortTxIds.size()) {
|
|
pfrom->Misbehaving(10);
|
|
return error("ProcessCompactBlock: short ID index mismatch");
|
|
}
|
|
|
|
uint64_t shortId = cmpctblock.vShortTxIds[nShortIdx++];
|
|
|
|
// Search mempool for matching short ID.
|
|
// Use the legacy GetShortTxId from main.h (which both sender and
|
|
// receiver must agree on). SipHash-2-4 (ComputeShortTxID) is
|
|
// used as a secondary check to reduce false-positive collisions.
|
|
bool fFound = false;
|
|
int nCollisions = 0;
|
|
{
|
|
LOCK(mempool.cs);
|
|
for (const auto& entry : mempool.mapTx)
|
|
{
|
|
if (GetShortTxId(entry.first, cmpctblock.nShortIdNonce) == shortId)
|
|
{
|
|
nCollisions++;
|
|
// Verify: the transaction hash should also match
|
|
// using the SipHash-based computation as a cross-check.
|
|
// If collisions exist, we can't disambiguate — request the tx.
|
|
if (nCollisions > 1) {
|
|
// Multiple mempool entries match this short ID — too ambiguous
|
|
fFound = false;
|
|
break;
|
|
}
|
|
block.vtx[i] = entry.second;
|
|
fFound = true;
|
|
}
|
|
}
|
|
}
|
|
if (!fFound)
|
|
setMissing.insert(i);
|
|
}
|
|
|
|
if (setMissing.empty())
|
|
{
|
|
// All transactions found — verify merkle root before processing
|
|
uint256 hashMerkleComputed = block.BuildMerkleTree();
|
|
if (hashMerkleComputed != block.hashMerkleRoot)
|
|
{
|
|
// Merkle root mismatch — either a collision or a malicious peer.
|
|
// Fall back to requesting the full block.
|
|
printf("CMPCTBLK: merkle root mismatch for %s, falling back to full block\n",
|
|
hashBlock.ToString().substr(0,20).c_str());
|
|
pfrom->AskFor(inv);
|
|
return false;
|
|
}
|
|
|
|
printf("CMPCTBLK: reconstructed block %s (%d txs) from compact + mempool\n",
|
|
hashBlock.ToString().substr(0,20).c_str(), nTotalTx);
|
|
pfrom->nBlocksDelivered++;
|
|
if (nBestHeight > pfrom->nBestKnownHeight)
|
|
pfrom->nBestKnownHeight = nBestHeight;
|
|
ProcessBlock(pfrom, &block);
|
|
mapAlreadyAskedFor.erase(inv);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Store partial block and request missing transactions
|
|
printf("CMPCTBLK: block %s missing %d txs, requesting\n",
|
|
hashBlock.ToString().substr(0,20).c_str(), (int)setMissing.size());
|
|
|
|
// Evict oldest partial blocks if at limit
|
|
while (mapPartialBlocks.size() >= MAX_PARTIAL_BLOCKS)
|
|
{
|
|
auto oldest = mapPartialBlocks.begin();
|
|
for (auto it = mapPartialBlocks.begin(); it != mapPartialBlocks.end(); ++it)
|
|
if (it->second.nReceiveTime < oldest->second.nReceiveTime)
|
|
oldest = it;
|
|
mapPartialBlocks.erase(oldest);
|
|
}
|
|
|
|
CPartialBlock partial;
|
|
partial.cmpctblock = cmpctblock;
|
|
partial.vTxFilled = block.vtx;
|
|
partial.setMissing = setMissing;
|
|
partial.nReceiveTime = GetTime();
|
|
partial.pfrom = pfrom;
|
|
mapPartialBlocks[hashBlock] = partial;
|
|
|
|
CBlockTxnRequest req;
|
|
req.blockhash = hashBlock;
|
|
req.vIndex.assign(setMissing.begin(), setMissing.end());
|
|
pfrom->PushMessage("getblocktxn", req);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Evict expired partial compact blocks (called periodically). */
|
|
static void CleanupPartialBlocks()
|
|
{
|
|
if (mapPartialBlocks.empty())
|
|
return;
|
|
int64_t nNow = GetTime();
|
|
for (auto it = mapPartialBlocks.begin(); it != mapPartialBlocks.end(); )
|
|
{
|
|
if (nNow - it->second.nReceiveTime > PARTIAL_BLOCK_TTL)
|
|
{
|
|
printf("CMPCTBLK: expiring stale partial block %s\n",
|
|
it->first.ToString().substr(0,20).c_str());
|
|
it = mapPartialBlocks.erase(it);
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// Constant stuff for coinbase transactions we create:
|
|
CScript COINBASE_FLAGS;
|
|
|
|
const string strMessageMagic = "Triangles Signed Message:\n";
|
|
|
|
|
|
|
|
// Settings
|
|
int64_t nTransactionFee = MIN_TX_FEE;
|
|
int64_t nReserveBalance = 0;
|
|
int64_t nMinimumInputValue = 0;
|
|
|
|
extern enum Checkpoints::CPMode CheckpointsMode;
|
|
|
|
namespace
|
|
{
|
|
|
|
static CCriticalSection cs_PostIbdWork;
|
|
static bool fPostIbdWorkStarted = false;
|
|
|
|
static void ThreadPostIbdWork(void* parg)
|
|
{
|
|
RenameThread("Triangles-postibd");
|
|
|
|
try
|
|
{
|
|
if (!fShutdown && pwalletMain && GetBoolArg("-postibdrescan", true))
|
|
{
|
|
printf("Starting post-IBD wallet rescan from genesis in background...\n");
|
|
uiInterface.InitMessage(_("Rescanning wallet in background..."));
|
|
int nFound = 0;
|
|
bool fUsedIndex = false;
|
|
if (fAddressIndex)
|
|
{
|
|
fUsedIndex = pwalletMain->ScanForWalletTransactionsFromIndex(pindexGenesisBlock, true, &nFound);
|
|
if (!fUsedIndex)
|
|
printf("Indexed wallet rescan failed, falling back to full rescan.\n");
|
|
}
|
|
if (!fUsedIndex)
|
|
nFound = pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
|
printf("Post-IBD wallet rescan complete: %d transactions found (indexed=%d)\n", nFound, fUsedIndex);
|
|
}
|
|
|
|
if (!fShutdown && fSecMsgEnabled)
|
|
{
|
|
printf("Starting post-IBD secure message chain scan in background...\n");
|
|
uiInterface.InitMessage(_("Scanning for secure messages in background..."));
|
|
SecureMsgScanBlockChain();
|
|
printf("Post-IBD secure message chain scan complete\n");
|
|
}
|
|
|
|
// If a canonical UTXO snapshot file is present in the data dir and its
|
|
// hash matches the compiled-in snapshot hash for this height, advertise
|
|
// NODE_SNAPSHOT so other peers can fetch it from us.
|
|
if (!fShutdown)
|
|
SnapshotNet::EnsureLocalSnapshot();
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
PrintExceptionContinue(&e, "ThreadPostIbdWork()");
|
|
}
|
|
catch (...)
|
|
{
|
|
PrintExceptionContinue(nullptr, "ThreadPostIbdWork()");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// dispatching functions
|
|
//
|
|
|
|
// These functions dispatch to one or all registered wallets
|
|
|
|
|
|
void RegisterWallet(CWallet* pwalletIn)
|
|
{
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
setpwalletRegistered.insert(pwalletIn);
|
|
}
|
|
}
|
|
|
|
void UnregisterWallet(CWallet* pwalletIn)
|
|
{
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
setpwalletRegistered.erase(pwalletIn);
|
|
}
|
|
}
|
|
|
|
// check whether the passed transaction is from us
|
|
bool static IsFromMe(CTransaction& tx)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
if (pwallet->IsFromMe(tx))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// get the wallet transaction with the given hash (if it exists)
|
|
bool static GetTransaction(const uint256& hashTx, CWalletTx& wtx)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
if (pwallet->GetTransaction(hashTx,wtx))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// erases transaction with the given hash from all wallets
|
|
void static EraseFromWallets(uint256 hash)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->EraseFromWallet(hash);
|
|
}
|
|
|
|
// make sure all wallets know about the given transaction, in the given block
|
|
void SyncWithWallets(const CTransaction& tx, const CBlock* pblock, bool fUpdate, bool fConnect)
|
|
{
|
|
if (!fConnect)
|
|
{
|
|
// triangles: wallets need to refund inputs when disconnecting coinstake
|
|
if (tx.IsCoinStake())
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
if (pwallet->IsFromMe(tx))
|
|
pwallet->DisableTransaction(tx);
|
|
}
|
|
return;
|
|
}
|
|
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->AddToWalletIfInvolvingMe(tx, pblock, fUpdate);
|
|
}
|
|
|
|
// notify wallets about a new best chain
|
|
void static SetBestChain(const CBlockLocator& loc)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->SetBestChain(loc);
|
|
}
|
|
|
|
static bool UpdateAddressIndexSyncState(CTxDBBase& txdb, const CBlockIndex* pindexNew)
|
|
{
|
|
if (!fAddressIndex || pindexNew == nullptr)
|
|
return true;
|
|
|
|
int nStartHeight = 0;
|
|
if (!txdb.ReadAddressIndexStartHeight(nStartHeight))
|
|
{
|
|
if (!txdb.WriteAddressIndexStartHeight(pindexNew->nHeight))
|
|
return false;
|
|
}
|
|
|
|
return txdb.WriteAddressIndexBestChain(pindexNew->GetBlockHash());
|
|
}
|
|
|
|
// notify wallets about an updated transaction
|
|
void static UpdatedTransaction(const uint256& hashTx)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->UpdatedTransaction(hashTx);
|
|
}
|
|
|
|
// dump all wallets
|
|
void static PrintWallets(const CBlock& block)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->PrintWallet(block);
|
|
}
|
|
|
|
// notify wallets about an incoming inventory (for request counts)
|
|
void static Inventory(const uint256& hash)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->Inventory(hash);
|
|
}
|
|
|
|
// ask wallets to resend their transactions
|
|
void ResendWalletTransactions(bool fForce)
|
|
{
|
|
LOCK(cs_setpwalletRegistered);
|
|
for (CWallet* pwallet : setpwalletRegistered)
|
|
pwallet->ResendWalletTransactions(fForce);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// mapOrphanTransactions
|
|
//
|
|
|
|
bool AddOrphanTx(const CTransaction& tx)
|
|
{
|
|
uint256 hash = tx.GetHash();
|
|
if (mapOrphanTransactions.count(hash))
|
|
return false;
|
|
|
|
// Ignore big transactions, to avoid a
|
|
// send-big-orphans memory exhaustion attack. If a peer has a legitimate
|
|
// large transaction with a missing parent then we assume
|
|
// it will rebroadcast it later, after the parent transaction(s)
|
|
// have been mined or received.
|
|
// 10,000 orphans, each of which is at most 5,000 bytes big is
|
|
// at most 500 megabytes of orphans:
|
|
|
|
size_t nSize = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION);
|
|
|
|
if (nSize > 5000)
|
|
{
|
|
printf("ignoring large orphan tx (size: %" PRIszu ", hash: %s)\n", nSize, hash.ToString().substr(0,10).c_str());
|
|
return false;
|
|
}
|
|
|
|
mapOrphanTransactions[hash] = tx;
|
|
for (const CTxIn& txin : tx.vin)
|
|
mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash);
|
|
|
|
if (fDebug)
|
|
printf("stored orphan tx %s (mapsz %" PRIszu ")\n", hash.ToString().substr(0,10).c_str(),
|
|
mapOrphanTransactions.size());
|
|
return true;
|
|
}
|
|
|
|
void static EraseOrphanTx(uint256 hash)
|
|
{
|
|
if (!mapOrphanTransactions.count(hash))
|
|
return;
|
|
const CTransaction& tx = mapOrphanTransactions[hash];
|
|
for (const CTxIn& txin : tx.vin)
|
|
{
|
|
mapOrphanTransactionsByPrev[txin.prevout.hash].erase(hash);
|
|
if (mapOrphanTransactionsByPrev[txin.prevout.hash].empty())
|
|
mapOrphanTransactionsByPrev.erase(txin.prevout.hash);
|
|
}
|
|
mapOrphanTransactions.erase(hash);
|
|
}
|
|
|
|
unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans)
|
|
{
|
|
unsigned int nEvicted = 0;
|
|
while (mapOrphanTransactions.size() > nMaxOrphans)
|
|
{
|
|
// Evict a random orphan:
|
|
uint256 randomhash = GetRandHash();
|
|
auto it = mapOrphanTransactions.lower_bound(randomhash);
|
|
if (it == mapOrphanTransactions.end())
|
|
it = mapOrphanTransactions.begin();
|
|
EraseOrphanTx(it->first);
|
|
++nEvicted;
|
|
}
|
|
return nEvicted;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CTransaction and CTxIndex
|
|
//
|
|
|
|
bool CTransaction::ReadFromDisk(CTxDBBase& txdb, COutPoint prevout, CTxIndex& txindexRet)
|
|
{
|
|
SetNull();
|
|
if (!txdb.ReadTxIndex(prevout.hash, txindexRet))
|
|
return false;
|
|
if (!ReadFromDisk(txindexRet.pos))
|
|
return false;
|
|
if (prevout.n >= vout.size())
|
|
{
|
|
SetNull();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CTransaction::ReadFromDisk(CTxDBBase& txdb, COutPoint prevout)
|
|
{
|
|
CTxIndex txindex;
|
|
return ReadFromDisk(txdb, prevout, txindex);
|
|
}
|
|
|
|
bool CTransaction::ReadFromDisk(COutPoint prevout)
|
|
{
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
CTxIndex txindex;
|
|
return ReadFromDisk(txdb, prevout, txindex);
|
|
}
|
|
|
|
bool CTransaction::IsStandard() const
|
|
{
|
|
if (nVersion > CTransaction::CURRENT_VERSION)
|
|
return false;
|
|
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
// Biggest 'standard' txin is a 3-signature 3-of-3 CHECKMULTISIG
|
|
// pay-to-script-hash, which is 3 ~80-byte signatures, 3
|
|
// ~65-byte public keys, plus a few script ops.
|
|
if (txin.scriptSig.size() > 500)
|
|
return false;
|
|
if (!txin.scriptSig.IsPushOnly())
|
|
return false;
|
|
if (fEnforceCanonical && !txin.scriptSig.HasCanonicalPushes()) {
|
|
return false;
|
|
}
|
|
}
|
|
for (const CTxOut& txout : vout) {
|
|
if (!::IsStandard(txout.scriptPubKey))
|
|
return false;
|
|
if (txout.nValue == 0)
|
|
return false;
|
|
if (fEnforceCanonical && !txout.scriptPubKey.HasCanonicalPushes()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// Check transaction inputs, and make sure any
|
|
// pay-to-script-hash transactions are evaluating IsStandard scripts
|
|
//
|
|
// Why bother? To avoid denial-of-service attacks; an attacker
|
|
// can submit a standard HASH... OP_EQUAL transaction,
|
|
// which will get accepted into blocks. The redemption
|
|
// script can be anything; an attacker could use a very
|
|
// expensive-to-check-upon-redemption script like:
|
|
// DUP CHECKSIG DROP ... repeated 100 times... OP_1
|
|
//
|
|
bool CTransaction::AreInputsStandard(const MapPrevTx& mapInputs) const
|
|
{
|
|
if (IsCoinBase())
|
|
return true; // Coinbases don't use vin normally
|
|
|
|
for (unsigned int i = 0; i < vin.size(); i++)
|
|
{
|
|
auto mi = mapInputs.find(vin[i].prevout);
|
|
if (mi == mapInputs.end())
|
|
return false;
|
|
const CUtxoEntry& entry = mi->second;
|
|
|
|
vector<vector<unsigned char> > vSolutions;
|
|
TxnOutType whichType;
|
|
const CScript& prevScript = entry.scriptPubKey;
|
|
if (!Solver(prevScript, whichType, vSolutions))
|
|
return false;
|
|
int nArgsExpected = ScriptSigArgsExpected(whichType, vSolutions);
|
|
if (nArgsExpected < 0)
|
|
return false;
|
|
|
|
vector<vector<unsigned char> > stack;
|
|
if (!EvalScript(stack, vin[i].scriptSig, *this, i, 0))
|
|
return false;
|
|
|
|
if (whichType == TxnOutType::ScriptHash)
|
|
{
|
|
if (stack.empty())
|
|
return false;
|
|
CScript subscript(stack.back().begin(), stack.back().end());
|
|
vector<vector<unsigned char> > vSolutions2;
|
|
TxnOutType whichType2;
|
|
if (!Solver(subscript, whichType2, vSolutions2))
|
|
return false;
|
|
if (whichType2 == TxnOutType::ScriptHash)
|
|
return false;
|
|
|
|
int tmpExpected;
|
|
tmpExpected = ScriptSigArgsExpected(whichType2, vSolutions2);
|
|
if (tmpExpected < 0)
|
|
return false;
|
|
nArgsExpected += tmpExpected;
|
|
}
|
|
|
|
if (stack.size() != (unsigned int)nArgsExpected)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
unsigned int
|
|
CTransaction::GetLegacySigOpCount() const
|
|
{
|
|
unsigned int nSigOps = 0;
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
nSigOps += txin.scriptSig.GetSigOpCount(false);
|
|
}
|
|
for (const CTxOut& txout : vout)
|
|
{
|
|
nSigOps += txout.scriptPubKey.GetSigOpCount(false);
|
|
}
|
|
return nSigOps;
|
|
}
|
|
|
|
|
|
int CMerkleTx::SetMerkleBranch(const CBlock* pblock)
|
|
{
|
|
if (fClient)
|
|
{
|
|
if (hashBlock == 0)
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
CBlock blockTmp;
|
|
if (pblock == nullptr)
|
|
{
|
|
// Load the block this tx is in
|
|
CTxIndex txindex;
|
|
if (!MakeChainDB("r")->ReadTxIndex(GetHash(), txindex))
|
|
return 0;
|
|
if (!blockTmp.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos))
|
|
return 0;
|
|
pblock = &blockTmp;
|
|
}
|
|
|
|
// Update the tx's hashBlock
|
|
hashBlock = pblock->GetHash();
|
|
|
|
// Locate the transaction
|
|
for (nIndex = 0; nIndex < (int)pblock->vtx.size(); nIndex++)
|
|
if (pblock->vtx[nIndex] == (const CTransaction&)*this)
|
|
break;
|
|
if (nIndex == (int)pblock->vtx.size())
|
|
{
|
|
vMerkleBranch.clear();
|
|
nIndex = -1;
|
|
printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n");
|
|
return 0;
|
|
}
|
|
|
|
// Fill in merkle branch
|
|
vMerkleBranch = pblock->GetMerkleBranch(nIndex);
|
|
}
|
|
|
|
// Is the tx in a block that's in the main chain
|
|
auto mi = mapBlockIndex.find(hashBlock);
|
|
if (mi == mapBlockIndex.end())
|
|
return 0;
|
|
CBlockIndex* pindex = mi->second;
|
|
if (!pindex || !pindex->IsInMainChain())
|
|
return 0;
|
|
|
|
return pindexBest->nHeight - pindex->nHeight + 1;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool CTransaction::CheckTransaction() const
|
|
{
|
|
// Basic checks that don't depend on any context
|
|
if (vin.empty())
|
|
return DoS(10, error("CTransaction::CheckTransaction() : vin empty"));
|
|
if (vout.empty())
|
|
return DoS(10, error("CTransaction::CheckTransaction() : vout empty"));
|
|
// Size limits
|
|
if (::GetSerializeSize(*this, SER_NETWORK, PROTOCOL_VERSION) > MAX_BLOCK_SIZE)
|
|
return DoS(100, error("CTransaction::CheckTransaction() : size limits failed"));
|
|
|
|
// Check for negative or overflow output values
|
|
int64_t nValueOut = 0;
|
|
for (const CTxOut& txout : vout)
|
|
{
|
|
if (txout.IsEmpty() && !IsCoinBase() && !IsCoinStake())
|
|
return DoS(100, error("CTransaction::CheckTransaction() : txout empty for user transaction"));
|
|
if (txout.nValue < 0)
|
|
return DoS(100, error("CTransaction::CheckTransaction() : txout.nValue negative"));
|
|
if (txout.nValue > MAX_MONEY)
|
|
return DoS(100, error("CTransaction::CheckTransaction() : txout.nValue too high"));
|
|
nValueOut += txout.nValue;
|
|
if (!MoneyRange(nValueOut))
|
|
return DoS(100, error("CTransaction::CheckTransaction() : txout total out of range"));
|
|
}
|
|
|
|
// Check for duplicate inputs
|
|
set<COutPoint> vInOutPoints;
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
if (vInOutPoints.count(txin.prevout))
|
|
return false;
|
|
vInOutPoints.insert(txin.prevout);
|
|
}
|
|
|
|
if (IsCoinBase())
|
|
{
|
|
if (vin[0].scriptSig.size() < 2 || vin[0].scriptSig.size() > 100)
|
|
return DoS(100, error("CTransaction::CheckTransaction() : coinbase script size is invalid"));
|
|
}
|
|
else
|
|
{
|
|
for (const CTxIn& txin : vin)
|
|
if (txin.prevout.IsNull())
|
|
return DoS(10, error("CTransaction::CheckTransaction() : prevout is null"));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int64_t CTransaction::GetMinFee(unsigned int nBlockSize, GetMinFeeMode mode, unsigned int nBytes) const
|
|
{
|
|
// Base fee is either MIN_TX_FEE or MIN_RELAY_TX_FEE
|
|
int64_t nBaseFee = (mode == GetMinFeeMode::Relay) ? MIN_RELAY_TX_FEE : MIN_TX_FEE;
|
|
|
|
unsigned int nNewBlockSize = nBlockSize + nBytes;
|
|
int64_t nMinFee = (1 + (int64_t)nBytes / 1000) * nBaseFee;
|
|
|
|
// To limit dust spam, require MIN_TX_FEE/MIN_RELAY_TX_FEE if any output is less than 0.01
|
|
if (nMinFee < nBaseFee)
|
|
{
|
|
for (const CTxOut& txout : vout)
|
|
if (txout.nValue < CENT)
|
|
nMinFee = nBaseFee;
|
|
}
|
|
|
|
// Raise the price as the block approaches full
|
|
if (nBlockSize != 1 && nNewBlockSize >= MAX_BLOCK_SIZE_GEN/2)
|
|
{
|
|
if (nNewBlockSize >= MAX_BLOCK_SIZE_GEN)
|
|
return MAX_MONEY;
|
|
nMinFee *= MAX_BLOCK_SIZE_GEN / (MAX_BLOCK_SIZE_GEN - nNewBlockSize);
|
|
}
|
|
|
|
if (!MoneyRange(nMinFee))
|
|
nMinFee = MAX_MONEY;
|
|
return nMinFee;
|
|
}
|
|
|
|
|
|
bool CTxMemPool::accept(CTxDBBase& txdb, CTransaction &tx, bool fCheckInputs,
|
|
bool* pfMissingInputs)
|
|
{
|
|
if (pfMissingInputs)
|
|
*pfMissingInputs = false;
|
|
|
|
if (!tx.CheckTransaction())
|
|
return error("CTxMemPool::accept() : CheckTransaction failed");
|
|
|
|
// Coinbase is only valid in a block, not as a loose transaction
|
|
if (tx.IsCoinBase())
|
|
return tx.DoS(100, error("CTxMemPool::accept() : coinbase as individual tx"));
|
|
|
|
// triangles: coinstake is also only valid in a block, not as a loose transaction
|
|
if (tx.IsCoinStake())
|
|
return tx.DoS(100, error("CTxMemPool::accept() : coinstake as individual tx"));
|
|
|
|
// To help v0.1.5 clients who would see it as a negative number
|
|
if ((int64_t)tx.nLockTime > std::numeric_limits<int>::max())
|
|
return error("CTxMemPool::accept() : not accepting nLockTime beyond 2038 yet");
|
|
|
|
// Rather not work on nonstandard transactions (unless -testnet)
|
|
|
|
if (!fTestNet && !tx.IsStandard())
|
|
return error("CTxMemPool::accept() : nonstandard transaction type");
|
|
|
|
// Do we already have it?
|
|
uint256 hash = tx.GetHash();
|
|
{
|
|
LOCK(cs);
|
|
if (mapTx.count(hash))
|
|
return false;
|
|
}
|
|
if (fCheckInputs)
|
|
if (txdb.ContainsTx(hash))
|
|
return false;
|
|
|
|
// Check for conflicts with in-memory transactions
|
|
CTransaction* ptxOld = nullptr;
|
|
for (unsigned int i = 0; i < tx.vin.size(); i++)
|
|
{
|
|
COutPoint outpoint = tx.vin[i].prevout;
|
|
if (mapNextTx.count(outpoint))
|
|
{
|
|
// Disable replacement feature for now
|
|
return false;
|
|
|
|
// Allow replacing with a newer version of the same transaction
|
|
if (i != 0)
|
|
return false;
|
|
ptxOld = mapNextTx[outpoint].ptx;
|
|
if (ptxOld->IsFinal())
|
|
return false;
|
|
if (!tx.IsNewerThan(*ptxOld))
|
|
return false;
|
|
for (unsigned int i = 0; i < tx.vin.size(); i++)
|
|
{
|
|
COutPoint outpoint = tx.vin[i].prevout;
|
|
if (!mapNextTx.count(outpoint) || mapNextTx[outpoint].ptx != ptxOld)
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fCheckInputs)
|
|
{
|
|
MapPrevTx mapInputs;
|
|
MapPrevTx mapEmpty; // no pending UTXOs for mempool acceptance
|
|
bool fInvalid = false;
|
|
if (!tx.FetchInputs(txdb, mapEmpty, false, false, mapInputs, fInvalid))
|
|
{
|
|
if (fInvalid)
|
|
return error("CTxMemPool::accept() : FetchInputs found invalid tx %s", hash.ToString().substr(0,10).c_str());
|
|
if (pfMissingInputs)
|
|
*pfMissingInputs = true;
|
|
return false;
|
|
}
|
|
|
|
// Check for non-standard pay-to-script-hash in inputs
|
|
if (!tx.AreInputsStandard(mapInputs) && !fTestNet)
|
|
return error("CTxMemPool::accept() : nonstandard transaction input");
|
|
|
|
// Note: if you modify this code to accept non-standard transactions, then
|
|
// you should add code here to check that the transaction does a
|
|
// reasonable number of ECDSA signature verifications.
|
|
|
|
int64_t nFees = tx.GetValueIn(mapInputs)-tx.GetValueOut();
|
|
unsigned int nSize = ::GetSerializeSize(tx, SER_NETWORK, PROTOCOL_VERSION);
|
|
|
|
// Don't accept it if it can't get into a block
|
|
int64_t txMinFee = tx.GetMinFee(1000, GetMinFeeMode::Relay, nSize);
|
|
if (nFees < txMinFee)
|
|
return error("CTxMemPool::accept() : not enough fees %s, %" PRId64 " < %" PRId64 ,
|
|
hash.ToString().c_str(),
|
|
nFees, txMinFee);
|
|
|
|
// Continuously rate-limit free transactions
|
|
// This mitigates 'penny-flooding' -- sending thousands of free transactions just to
|
|
// be annoying or make others' transactions take longer to confirm.
|
|
if (nFees < MIN_RELAY_TX_FEE)
|
|
{
|
|
static CCriticalSection cs;
|
|
static double dFreeCount;
|
|
static int64_t nLastTime;
|
|
int64_t nNow = GetTime();
|
|
|
|
{
|
|
LOCK(cs);
|
|
// Use an exponentially decaying ~10-minute window:
|
|
dFreeCount *= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime));
|
|
nLastTime = nNow;
|
|
// -limitfreerelay unit is thousand-bytes-per-minute
|
|
// At default rate it would take over a month to fill 1GB
|
|
if (dFreeCount > GetArg("-limitfreerelay", 15)*10*1000 && !IsFromMe(tx))
|
|
return error("CTxMemPool::accept() : free transaction rejected by rate limiter");
|
|
if (fDebug)
|
|
printf("Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize);
|
|
dFreeCount += nSize;
|
|
}
|
|
}
|
|
|
|
// Check against previous transactions
|
|
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
|
|
if (!tx.ConnectInputs(txdb, mapInputs, pindexBest, false, false))
|
|
{
|
|
return error("CTxMemPool::accept() : ConnectInputs failed %s", hash.ToString().substr(0,10).c_str());
|
|
}
|
|
}
|
|
|
|
// Store transaction in memory
|
|
{
|
|
LOCK(cs);
|
|
if (ptxOld)
|
|
{
|
|
printf("CTxMemPool::accept() : replacing tx %s with new version\n", ptxOld->GetHash().ToString().c_str());
|
|
remove(*ptxOld);
|
|
}
|
|
addUnchecked(hash, tx);
|
|
}
|
|
|
|
///// are we sure this is ok when loading transactions or restoring block txes
|
|
// If updated, erase old tx from wallet
|
|
if (ptxOld)
|
|
EraseFromWallets(ptxOld->GetHash());
|
|
|
|
printf("CTxMemPool::accept() : accepted %s (poolsz %" PRIszu ")\n",
|
|
hash.ToString().substr(0,10).c_str(),
|
|
mapTx.size());
|
|
|
|
#ifdef ENABLE_ZMQ
|
|
if (pzmqNotifier)
|
|
pzmqNotifier->NotifyTransactionHash(hash);
|
|
#endif
|
|
|
|
// SSE notification for new mempool transaction
|
|
if (pNotificationQueue)
|
|
pNotificationQueue->Push("{\"type\":\"tx\",\"hash\":\"" + hash.GetHex() + "\"}");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CTransaction::AcceptToMemoryPool(CTxDBBase& txdb, bool fCheckInputs, bool* pfMissingInputs)
|
|
{
|
|
return mempool.accept(txdb, *this, fCheckInputs, pfMissingInputs);
|
|
}
|
|
|
|
bool CTxMemPool::addUnchecked(const uint256& hash, CTransaction &tx)
|
|
{
|
|
// Add to memory pool without checking anything. Don't call this directly,
|
|
// call CTxMemPool::accept to properly check the transaction first.
|
|
{
|
|
mapTx[hash] = tx;
|
|
for (unsigned int i = 0; i < tx.vin.size(); i++)
|
|
mapNextTx[tx.vin[i].prevout] = CInPoint(&mapTx[hash], i);
|
|
nTransactionsUpdated++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool CTxMemPool::remove(const CTransaction &tx, bool fRecursive)
|
|
{
|
|
// Remove transaction from memory pool
|
|
{
|
|
LOCK(cs);
|
|
uint256 hash = tx.GetHash();
|
|
if (mapTx.count(hash))
|
|
{
|
|
if (fRecursive) {
|
|
for (unsigned int i = 0; i < tx.vout.size(); i++) {
|
|
auto it = mapNextTx.find(COutPoint(hash, i));
|
|
if (it != mapNextTx.end())
|
|
remove(*it->second.ptx, true);
|
|
}
|
|
}
|
|
for (const CTxIn& txin : tx.vin)
|
|
mapNextTx.erase(txin.prevout);
|
|
mapTx.erase(hash);
|
|
nTransactionsUpdated++;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CTxMemPool::removeConflicts(const CTransaction &tx)
|
|
{
|
|
// Remove transactions which depend on inputs of tx, recursively
|
|
LOCK(cs);
|
|
for (const CTxIn &txin : tx.vin) {
|
|
auto it = mapNextTx.find(txin.prevout);
|
|
if (it != mapNextTx.end()) {
|
|
const CTransaction &txConflict = *it->second.ptx;
|
|
if (txConflict != tx)
|
|
remove(txConflict, true);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CTxMemPool::clear()
|
|
{
|
|
LOCK(cs);
|
|
mapTx.clear();
|
|
mapNextTx.clear();
|
|
++nTransactionsUpdated;
|
|
}
|
|
|
|
void CTxMemPool::queryHashes(std::vector<uint256>& vtxid)
|
|
{
|
|
vtxid.clear();
|
|
|
|
LOCK(cs);
|
|
vtxid.reserve(mapTx.size());
|
|
for (const auto& [hash, tx] : mapTx)
|
|
vtxid.push_back(hash);
|
|
}
|
|
|
|
|
|
|
|
|
|
int CMerkleTx::GetDepthInMainChainINTERNAL(CBlockIndex* &pindexRet) const
|
|
{
|
|
if (hashBlock == 0 || nIndex == -1)
|
|
return 0;
|
|
|
|
// Find the block it claims to be in
|
|
auto mi = mapBlockIndex.find(hashBlock);
|
|
if (mi == mapBlockIndex.end())
|
|
return 0;
|
|
CBlockIndex* pindex = mi->second;
|
|
if (!pindex || !pindex->IsInMainChain())
|
|
return 0;
|
|
|
|
if (!fMerkleVerified)
|
|
{
|
|
if (CBlock::CheckMerkleBranch(GetHash(), vMerkleBranch, nIndex) != pindex->hashMerkleRoot)
|
|
return 0;
|
|
fMerkleVerified = true;
|
|
}
|
|
|
|
pindexRet = pindex;
|
|
return pindexBest->nHeight - pindex->nHeight + 1;
|
|
}
|
|
|
|
int CMerkleTx::GetDepthInMainChain(CBlockIndex* &pindexRet) const
|
|
{
|
|
int nResult = GetDepthInMainChainINTERNAL(pindexRet);
|
|
if (nResult == 0 && !mempool.exists(GetHash()))
|
|
return -1; // Not in chain, not in mempool
|
|
|
|
return nResult;
|
|
}
|
|
|
|
int CMerkleTx::GetBlocksToMaturity() const
|
|
{
|
|
if (!(IsCoinBase() || IsCoinStake()))
|
|
return 0;
|
|
return max(0, (nCoinbaseMaturity) - GetDepthInMainChain());
|
|
}
|
|
|
|
|
|
bool CMerkleTx::AcceptToMemoryPool(CTxDBBase& txdb, bool fCheckInputs)
|
|
{
|
|
if (fClient)
|
|
{
|
|
if (!IsInMainChain() && !ClientConnectInputs())
|
|
return false;
|
|
return CTransaction::AcceptToMemoryPool(txdb, fCheckInputs);
|
|
}
|
|
else
|
|
{
|
|
return CTransaction::AcceptToMemoryPool(txdb, fCheckInputs);
|
|
}
|
|
}
|
|
|
|
bool CMerkleTx::AcceptToMemoryPool()
|
|
{
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
return AcceptToMemoryPool(txdb);
|
|
}
|
|
|
|
|
|
|
|
bool CWalletTx::AcceptWalletTransaction(CTxDBBase& txdb, bool fCheckInputs)
|
|
{
|
|
|
|
{
|
|
LOCK(mempool.cs);
|
|
// Add previous supporting transactions first
|
|
for (CMerkleTx& tx : vtxPrev)
|
|
{
|
|
if (!(tx.IsCoinBase() || tx.IsCoinStake()))
|
|
{
|
|
uint256 hash = tx.GetHash();
|
|
if (!mempool.exists(hash) && !txdb.ContainsTx(hash))
|
|
tx.AcceptToMemoryPool(txdb, fCheckInputs);
|
|
}
|
|
}
|
|
return AcceptToMemoryPool(txdb, fCheckInputs);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CWalletTx::AcceptWalletTransaction()
|
|
{
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
return AcceptWalletTransaction(txdb);
|
|
}
|
|
|
|
int CTxIndex::GetDepthInMainChain() const
|
|
{
|
|
// Read block header
|
|
CBlock block;
|
|
if (!block.ReadFromDisk(pos.nFile, pos.nBlockPos, false))
|
|
return 0;
|
|
// Find the block in the index
|
|
auto mi = mapBlockIndex.find(block.GetHash());
|
|
if (mi == mapBlockIndex.end())
|
|
return 0;
|
|
CBlockIndex* pindex = mi->second;
|
|
if (!pindex || !pindex->IsInMainChain())
|
|
return 0;
|
|
return 1 + nBestHeight - pindex->nHeight;
|
|
}
|
|
|
|
// Return transaction in tx, and if it was found inside a block, its hash is placed in hashBlock
|
|
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock)
|
|
{
|
|
{
|
|
LOCK(cs_main);
|
|
{
|
|
LOCK(mempool.cs);
|
|
if (mempool.exists(hash))
|
|
{
|
|
tx = mempool.lookup(hash);
|
|
return true;
|
|
}
|
|
}
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
CTxIndex txindex;
|
|
if (tx.ReadFromDisk(txdb, COutPoint(hash, 0), txindex))
|
|
{
|
|
CBlock block;
|
|
if (block.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
|
|
hashBlock = block.GetHash();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CBlock and CBlockIndex
|
|
//
|
|
|
|
static CBlockIndex* pblockindexFBBHLast;
|
|
CBlockIndex* FindBlockByHeight(int nHeight)
|
|
{
|
|
CBlockIndex *pblockindex;
|
|
if (nHeight < nBestHeight / 2)
|
|
pblockindex = pindexGenesisBlock;
|
|
else
|
|
pblockindex = pindexBest;
|
|
if (!pblockindex)
|
|
return nullptr;
|
|
if (pblockindexFBBHLast && abs(nHeight - pblockindex->nHeight) > abs(nHeight - pblockindexFBBHLast->nHeight))
|
|
pblockindex = pblockindexFBBHLast;
|
|
while (pblockindex && pblockindex->nHeight > nHeight)
|
|
pblockindex = pblockindex->pprev;
|
|
while (pblockindex && pblockindex->nHeight < nHeight)
|
|
pblockindex = pblockindex->pnext;
|
|
if (!pblockindex || pblockindex->nHeight != nHeight)
|
|
return nullptr;
|
|
pblockindexFBBHLast = pblockindex;
|
|
return pblockindex;
|
|
}
|
|
|
|
bool CBlock::ReadFromDisk(const CBlockIndex* pindex, bool fReadTransactions)
|
|
{
|
|
if (!fReadTransactions)
|
|
{
|
|
*this = pindex->GetBlockHeader();
|
|
return true;
|
|
}
|
|
if (!ReadFromDisk(pindex->nFile, pindex->nBlockPos, fReadTransactions))
|
|
return false;
|
|
if (GetHash() != pindex->GetBlockHash())
|
|
return error("CBlock::ReadFromDisk() : GetHash() doesn't match index");
|
|
return true;
|
|
}
|
|
|
|
uint256 static GetOrphanRoot(const CBlock* pblock)
|
|
{
|
|
// Work back to the first block in the orphan chain
|
|
while (mapOrphanBlocks.count(pblock->hashPrevBlock))
|
|
pblock = mapOrphanBlocks[pblock->hashPrevBlock].get();
|
|
return pblock->GetHash();
|
|
}
|
|
|
|
// triangles: find block wanted by given orphan block
|
|
uint256 WantedByOrphan(const CBlock* pblockOrphan)
|
|
{
|
|
// Work back to the first block in the orphan chain
|
|
while (mapOrphanBlocks.count(pblockOrphan->hashPrevBlock))
|
|
pblockOrphan = mapOrphanBlocks[pblockOrphan->hashPrevBlock].get();
|
|
return pblockOrphan->hashPrevBlock;
|
|
}
|
|
|
|
// Track orphan insertion order for smart eviction (oldest first)
|
|
static std::deque<uint256> dequeOrphanOrder;
|
|
|
|
// Evict excess orphan blocks when limit is exceeded.
|
|
// Evicts oldest orphans first (FIFO) instead of random — this ensures
|
|
// legitimate out-of-order blocks from recent parallel downloads survive,
|
|
// while stale orphans that will likely never connect get cleaned up.
|
|
unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans)
|
|
{
|
|
unsigned int nEvicted = 0;
|
|
while (mapOrphanBlocks.size() > nMaxOrphans)
|
|
{
|
|
// Evict the oldest orphan (front of insertion queue)
|
|
while (!dequeOrphanOrder.empty() && !mapOrphanBlocks.count(dequeOrphanOrder.front()))
|
|
dequeOrphanOrder.pop_front(); // skip already-removed entries
|
|
|
|
if (dequeOrphanOrder.empty())
|
|
break;
|
|
|
|
uint256 evictHash = dequeOrphanOrder.front();
|
|
dequeOrphanOrder.pop_front();
|
|
|
|
auto it = mapOrphanBlocks.find(evictHash);
|
|
if (it == mapOrphanBlocks.end())
|
|
continue;
|
|
|
|
CBlock* pblockEvict = it->second.get();
|
|
|
|
// Remove from by-prev index
|
|
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
|
range.first != range.second; ++range.first)
|
|
{
|
|
if (range.first->second == pblockEvict) {
|
|
mapOrphanBlocksByPrev.erase(range.first);
|
|
break;
|
|
}
|
|
}
|
|
|
|
setStakeSeenOrphan.erase(pblockEvict->GetProofOfStake());
|
|
mapOrphanBlocks.erase(evictHash);
|
|
nEvicted++;
|
|
}
|
|
|
|
if (nEvicted > 0)
|
|
printf("LimitOrphanBlocks: evicted %u oldest orphan(s), %u remain\n",
|
|
nEvicted, (unsigned int)mapOrphanBlocks.size());
|
|
|
|
return nEvicted;
|
|
}
|
|
|
|
// miner's coin base reward
|
|
int64_t GetProofOfWorkReward(int64_t nFees)
|
|
{
|
|
int64_t nSubsidy = 1 * COIN;
|
|
|
|
if (!pindexBest)
|
|
return nSubsidy + nFees;
|
|
|
|
if (pindexBest->nHeight >= 9001) { nSubsidy = 0 * COIN; }
|
|
else if (pindexBest->nHeight >= 7000) { nSubsidy = 10 * COIN; }
|
|
else if (pindexBest->nHeight >= 3000) { nSubsidy = 5 * COIN; }
|
|
else if (pindexBest->nHeight >= 1000) { nSubsidy = 10 * COIN; }
|
|
else if (pindexBest->nHeight >= 100) { nSubsidy = 20 * COIN; }
|
|
else if (pindexBest->nHeight >= 1) { nSubsidy = 1 * COIN; }
|
|
|
|
if (fDebug && GetBoolArg("-printcreation"))
|
|
printf("GetProofOfWorkReward() : create=%s nSubsidy=%" PRId64 "\n", FormatMoney(nSubsidy).c_str(), nSubsidy);
|
|
|
|
return nSubsidy + nFees;
|
|
}
|
|
|
|
// miner's coin stake reward based on coin age spent (coin-days)
|
|
int64_t GetProofOfStakeReward(int64_t nCoinAge, int64_t nFees)
|
|
{
|
|
int64_t nRewardCoinYear;
|
|
|
|
nRewardCoinYear = MAX_TRI_PROOF_OF_STAKE;
|
|
|
|
CBigNum bnSubsidy;
|
|
bnSubsidy.SetCompact(0);
|
|
bnSubsidy = nCoinAge;
|
|
bnSubsidy *= nRewardCoinYear;
|
|
bnSubsidy /= 365;
|
|
bnSubsidy /= COIN;
|
|
|
|
int64_t nSubsidy = bnSubsidy.getuint64();
|
|
|
|
|
|
if (fDebug && GetBoolArg("-printcreation"))
|
|
printf("GetProofOfStakeReward(): create=%s nCoinAge=%" PRId64 "\n", FormatMoney(nSubsidy).c_str(), nCoinAge);
|
|
|
|
return nSubsidy + nFees;
|
|
}
|
|
|
|
static const int64_t nTargetTimespan = 60 * 30; // 30 mins
|
|
|
|
//
|
|
// maximum nBits value could possible be required nTime after
|
|
//
|
|
unsigned int ComputeMaxBits(CBigNum bnTargetLimit, unsigned int nBase, int64_t nTime)
|
|
{
|
|
CBigNum bnResult;
|
|
bnResult.SetCompact(nBase);
|
|
bnResult *= 2;
|
|
while (nTime > 0 && bnResult < bnTargetLimit)
|
|
{
|
|
// Maximum 200% adjustment per day...
|
|
bnResult *= 2;
|
|
nTime -= 24 * 60 * 60;
|
|
}
|
|
if (bnResult > bnTargetLimit)
|
|
bnResult = bnTargetLimit;
|
|
return bnResult.GetCompact();
|
|
}
|
|
|
|
//
|
|
// minimum amount of work that could possibly be required nTime after
|
|
// minimum proof-of-work required was nBase
|
|
//
|
|
unsigned int ComputeMinWork(unsigned int nBase, int64_t nTime)
|
|
{
|
|
return ComputeMaxBits(bnProofOfWorkLimit, nBase, nTime);
|
|
}
|
|
|
|
//
|
|
// minimum amount of stake that could possibly be required nTime after
|
|
// minimum proof-of-stake required was nBase
|
|
//
|
|
unsigned int ComputeMinStake(unsigned int nBase, int64_t nTime, unsigned int nBlockTime)
|
|
{
|
|
return ComputeMaxBits(bnProofOfStakeLimit, nBase, nTime);
|
|
}
|
|
|
|
|
|
// triangles: find last block index up to pindex
|
|
const CBlockIndex* GetLastBlockIndex(const CBlockIndex* pindex, bool fProofOfStake)
|
|
{
|
|
while (pindex && pindex->pprev && (pindex->IsProofOfStake() != fProofOfStake))
|
|
pindex = pindex->pprev;
|
|
return pindex;
|
|
}
|
|
|
|
static unsigned int GetNextTargetRequired_(const CBlockIndex* pindexLast, bool fProofOfStake)
|
|
{
|
|
CBigNum bnTargetLimit = fProofOfStake ? bnProofOfStakeLimit : bnProofOfWorkLimit;
|
|
|
|
if (pindexLast == nullptr)
|
|
return bnTargetLimit.GetCompact(); // genesis block
|
|
|
|
const CBlockIndex* pindexPrev = GetLastBlockIndex(pindexLast, fProofOfStake);
|
|
if (pindexPrev == nullptr)
|
|
return bnTargetLimit.GetCompact(); // no previous block of this type
|
|
if (pindexPrev->pprev == nullptr)
|
|
return bnTargetLimit.GetCompact(); // first block
|
|
const CBlockIndex* pindexPrevPrev = GetLastBlockIndex(pindexPrev->pprev, fProofOfStake);
|
|
if (pindexPrevPrev == nullptr)
|
|
return bnTargetLimit.GetCompact(); // no second previous block of this type
|
|
if (pindexPrevPrev->pprev == nullptr)
|
|
return bnTargetLimit.GetCompact(); // second block
|
|
|
|
int64_t nActualSpacing = pindexPrev->GetBlockTime() - pindexPrevPrev->GetBlockTime();
|
|
if(nActualSpacing < 0)
|
|
{
|
|
//printf(">> nActualSpacing = %" PRId64 " corrected to %" PRId64 "\n", nActualSpacing, nTargetSpacing);
|
|
nActualSpacing = nTargetSpacing;
|
|
}
|
|
|
|
// triangles: target change every block
|
|
// triangles: retarget with exponential moving toward target spacing
|
|
CBigNum bnNew;
|
|
bnNew.SetCompact(pindexPrev->nBits);
|
|
int64_t nInterval = nTargetTimespan / nTargetSpacing;
|
|
bnNew *= ((nInterval - 1) * nTargetSpacing + nActualSpacing + nActualSpacing);
|
|
bnNew /= ((nInterval + 1) * nTargetSpacing);
|
|
|
|
/*
|
|
printf(">> Height = %d, fProofOfStake = %d, nInterval = %" PRId64 ", nTargetSpacing = %" PRId64 ", nActualSpacing = %" PRId64 "\n",
|
|
pindexPrev->nHeight, fProofOfStake, nInterval, nTargetSpacing, nActualSpacing);
|
|
printf(">> pindexPrev->GetBlockTime() = %" PRId64 ", pindexPrev->nHeight = %d, pindexPrevPrev->GetBlockTime() = %" PRId64 ", pindexPrevPrev->nHeight = %d\n",
|
|
pindexPrev->GetBlockTime(), pindexPrev->nHeight, pindexPrevPrev->GetBlockTime(), pindexPrevPrev->nHeight);
|
|
*/
|
|
|
|
if (bnNew <= 0 || bnNew > bnTargetLimit)
|
|
bnNew = bnTargetLimit;
|
|
|
|
return bnNew.GetCompact();
|
|
}
|
|
|
|
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake)
|
|
{
|
|
// At fork height, reset PoS difficulty to minimum so staking can restart
|
|
if (pindexLast != nullptr && pindexLast->nHeight + 1 == FORK_HEIGHT_V5 && fProofOfStake)
|
|
return bnProofOfStakeLimit.GetCompact();
|
|
|
|
return GetNextTargetRequired_(pindexLast, fProofOfStake);
|
|
}
|
|
|
|
bool CheckProofOfWork(uint256 hash, unsigned int nBits)
|
|
{
|
|
CBigNum bnTarget;
|
|
bnTarget.SetCompact(nBits);
|
|
|
|
// Check range
|
|
if (bnTarget <= 0 || bnTarget > bnProofOfWorkLimit)
|
|
return error("CheckProofOfWork() : nBits below minimum work");
|
|
|
|
// Check proof of work matches claimed amount
|
|
if (hash > bnTarget.getuint256())
|
|
return error("CheckProofOfWork() : hash doesn't match nBits");
|
|
|
|
return true;
|
|
}
|
|
|
|
// Return maximum amount of blocks that other nodes claim to have
|
|
int GetNumBlocksOfPeers()
|
|
{
|
|
return std::max(cPeerBlockCounts.median(), Checkpoints::GetTotalBlocksEstimate());
|
|
}
|
|
|
|
bool IsStakingSafe(const CWallet* pwallet, const std::vector<CNode*>& vNodesSnapshot)
|
|
{
|
|
// (1) Never stake during IBD UNLESS we're caught up to peers. A node
|
|
// that is fully synced but idle (chain stalled >24h, so IBD flips
|
|
// true via the stale-tip heuristic) MUST keep staking so the network
|
|
// can self-heal. Without this carve-out, every node simultaneously
|
|
// refuses to stake after 24h of no blocks and the chain deadlocks.
|
|
//
|
|
// GetNumBlocksOfPeers() is the peer median height clamped to the
|
|
// checkpoint estimate, so this comparison is approximate: a node at
|
|
// the peer median clears it, a node behind does not.
|
|
if (IsInitialBlockDownload() && nBestHeight < GetNumBlocksOfPeers())
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (IBD)\n");
|
|
return false;
|
|
}
|
|
if (!pwallet)
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (no wallet)\n");
|
|
return false;
|
|
}
|
|
|
|
// (2) Require at least 2 fully handshaken, non-disconnecting peers.
|
|
int nLivePeers = 0;
|
|
for (CNode* pnode : vNodesSnapshot)
|
|
{
|
|
if (!pnode || pnode->fDisconnect)
|
|
continue;
|
|
// VERSION handshake complete: required to trust peer's tip data.
|
|
if (pnode->nVersion == 0)
|
|
continue;
|
|
nLivePeers++;
|
|
}
|
|
if (nLivePeers < 2)
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (only %d live peers, need >=2)\n", nLivePeers);
|
|
return false;
|
|
}
|
|
|
|
// (3) Refuse to stake while our height is behind the peer median.
|
|
int nPeerMedian = GetNumBlocksOfPeers();
|
|
if (nBestHeight < nPeerMedian)
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (our height %d behind peer median %d)\n",
|
|
nBestHeight, nPeerMedian);
|
|
return false;
|
|
}
|
|
|
|
// (4) Chain-trust vs. peers — the most we can honestly assert without
|
|
// peer-tip-hash state is that our cumulative chain trust has not
|
|
// fallen behind what peers report on nBestKnownHeight. If a peer's
|
|
// nBestKnownHeight is far beyond us, they may be on a competing fork.
|
|
// Until we add real peer-tip-hash protocol state, this is a
|
|
// conservative height+trust delta check.
|
|
if (pindexBest == nullptr)
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (no active chain)\n");
|
|
return false;
|
|
}
|
|
|
|
// If any peer reports a tip materially ahead of us (>=2 blocks), treat
|
|
// as a competing-fork signal and wait. This is the defensive layer;
|
|
// the full "competing valid fork at our trust level" check needs
|
|
// peer-tip-hash agreement, which is a separate protocol change.
|
|
for (CNode* pnode : vNodesSnapshot)
|
|
{
|
|
if (!pnode || pnode->fDisconnect || pnode->nVersion == 0)
|
|
continue;
|
|
if (pnode->nBestKnownHeight > nBestHeight + 2)
|
|
{
|
|
if (fDebug) printf("STAKING-GATE: refuse (peer reports height %d, well ahead of our %d — possible competing fork)\n",
|
|
pnode->nBestKnownHeight, nBestHeight);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsInitialBlockDownload()
|
|
{
|
|
// Bootstrap escape hatch: when the network has stalled and every node
|
|
// thinks it is in IBD because the tip is older than 24h, -forcestaking
|
|
// lets a single operator mint the first block to unstick the chain.
|
|
if (GetBoolArg("-forcestaking", false) && pindexBest != nullptr &&
|
|
nBestHeight >= Checkpoints::GetTotalBlocksEstimate())
|
|
return false;
|
|
if (pindexBest == nullptr || nBestHeight < Checkpoints::GetTotalBlocksEstimate())
|
|
return true;
|
|
static int64_t nLastUpdate;
|
|
static CBlockIndex* pindexLastBest;
|
|
if (pindexBest != pindexLastBest)
|
|
{
|
|
pindexLastBest = pindexBest;
|
|
nLastUpdate = GetTime();
|
|
}
|
|
// IBD is complete once we've passed the checkpoint AND the chain tip is
|
|
// recent (within 24h). This prevents a stall AFTER checkpoint from
|
|
// permanently disabling header fetching. The forcestaking path above
|
|
// handles the specific staking-broker scenario.
|
|
if (GetTime() - nLastUpdate > 24 * 60 * 60)
|
|
return true;
|
|
// Also enter IBD if we're significantly behind peer heights, even if
|
|
// the tip was recently updated (e.g. after a daemon restart on a
|
|
// stalled chain). Without this, a node that restarts on a frozen
|
|
// chain thinks it's fully synced (tip < 24h old from restart) and
|
|
// never requests blocks from peers — permanently stuck.
|
|
//
|
|
// Use the raw peer median (not GetNumBlocksOfPeers(), which clamps to
|
|
// the hardcoded checkpoint height). On a stalled chain where we're past
|
|
// the last checkpoint, GetNumBlocksOfPeers() returns the checkpoint
|
|
// height (2,214,400), not the actual peer height (2,224,763). Without
|
|
// using the raw median, a node at 2,219,922 with peers at 2,224,763
|
|
// would not detect it's behind.
|
|
int nPeerMedian = cPeerBlockCounts.median();
|
|
if (nPeerMedian > 0 && nBestHeight < nPeerMedian - 5)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool IsConsensusAssumeValidHeight(int nHeight)
|
|
{
|
|
return (nHeight <= Checkpoints::GetTotalBlocksEstimate())
|
|
|| (nHeight <= nAssumeValidThreshold);
|
|
}
|
|
|
|
bool IsBlockSignatureRequiredAtHeight(int nHeight)
|
|
{
|
|
return nHeight > Checkpoints::GetTotalBlocksEstimate();
|
|
}
|
|
|
|
void static InvalidChainFound(CBlockIndex* pindexNew)
|
|
{
|
|
if (pindexNew->nChainTrust > nBestInvalidTrust)
|
|
{
|
|
nBestInvalidTrust = pindexNew->nChainTrust;
|
|
MakeChainDB()->WriteBestInvalidTrust(CBigNum(nBestInvalidTrust));
|
|
uiInterface.NotifyBlocksChanged();
|
|
}
|
|
|
|
uint256 nBestInvalidBlockTrust = pindexNew->pprev
|
|
? pindexNew->nChainTrust - pindexNew->pprev->nChainTrust
|
|
: pindexNew->nChainTrust;
|
|
uint256 nBestBlockTrust = (pindexBest && pindexBest->nHeight != 0 && pindexBest->pprev)
|
|
? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust)
|
|
: (pindexBest ? pindexBest->nChainTrust : uint256(0));
|
|
|
|
printf("InvalidChainFound: invalid block=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s\n",
|
|
pindexNew->GetBlockHash().ToString().substr(0,20).c_str(), pindexNew->nHeight,
|
|
CBigNum(pindexNew->nChainTrust).ToString().c_str(), nBestInvalidBlockTrust.Get64(),
|
|
DateTimeStrFormat("%x %H:%M:%S", pindexNew->GetBlockTime()).c_str());
|
|
printf("InvalidChainFound: current best=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s\n",
|
|
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight,
|
|
pindexBest ? CBigNum(pindexBest->nChainTrust).ToString().c_str() : "0",
|
|
nBestBlockTrust.Get64(),
|
|
pindexBest ? DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str() : "unknown");
|
|
}
|
|
|
|
|
|
void CBlock::UpdateTime(const CBlockIndex* pindexPrev)
|
|
{
|
|
nTime = max(GetBlockTime(), GetAdjustedTime());
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool CTransaction::DisconnectInputs(CTxDBBase& txdb)
|
|
{
|
|
// Remove transaction position index entry.
|
|
// UTXO undo (restoring spent outputs, removing created outputs) is
|
|
// handled by DisconnectBlock's UTXO section.
|
|
txdb.EraseTxIndex(*this);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool CTransaction::FetchInputs(CTxDBBase& txdb, const MapPrevTx& mapPendingUtxos,
|
|
bool fBlock, bool fMiner, MapPrevTx& inputsRet, bool& fInvalid)
|
|
{
|
|
// FetchInputs can return false either because we just haven't seen some inputs
|
|
// (in which case the transaction should be stored as an orphan)
|
|
// or because the transaction is malformed (in which case the transaction should
|
|
// be dropped). If tx is definitely invalid, fInvalid will be set to true.
|
|
fInvalid = false;
|
|
|
|
if (IsCoinBase())
|
|
return true; // Coinbase transactions have no inputs to fetch.
|
|
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
COutPoint prevout = txin.prevout;
|
|
if (inputsRet.count(prevout))
|
|
continue; // Got it already
|
|
|
|
// Check pending UTXOs from earlier transactions in the same block
|
|
auto mi = mapPendingUtxos.find(prevout);
|
|
if (mi != mapPendingUtxos.end())
|
|
{
|
|
inputsRet[prevout] = mi->second;
|
|
continue;
|
|
}
|
|
|
|
// Read from UTXO database
|
|
CUtxoEntry entry;
|
|
if (txdb.ReadUtxo(prevout.hash, prevout.n, entry))
|
|
{
|
|
inputsRet[prevout] = entry;
|
|
continue;
|
|
}
|
|
|
|
// Not in UTXO DB — check mempool
|
|
{
|
|
LOCK(mempool.cs);
|
|
if (mempool.exists(prevout.hash))
|
|
{
|
|
const CTransaction& txPrev = mempool.lookup(prevout.hash);
|
|
if (prevout.n < txPrev.vout.size())
|
|
{
|
|
CUtxoEntry mempoolEntry;
|
|
mempoolEntry.nValue = txPrev.vout[prevout.n].nValue;
|
|
mempoolEntry.nHeight = 0; // not yet in a block
|
|
mempoolEntry.scriptPubKey = txPrev.vout[prevout.n].scriptPubKey;
|
|
mempoolEntry.fCoinBase = txPrev.IsCoinBase();
|
|
mempoolEntry.fCoinStake = txPrev.IsCoinStake();
|
|
mempoolEntry.nTxTime = txPrev.nTime;
|
|
inputsRet[prevout] = mempoolEntry;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Input not found anywhere
|
|
if (fBlock || fMiner)
|
|
return fMiner ? false : error("FetchInputs() : %s prev output %s:%d not found", GetHash().ToString().substr(0,10).c_str(), prevout.hash.ToString().substr(0,10).c_str(), prevout.n);
|
|
|
|
// For orphan detection in AcceptToMemoryPool
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const CTxOut& CTransaction::GetOutputFor(const CTxIn& input, const MapPrevTx& inputs) const
|
|
{
|
|
// Legacy adapter: constructs a temporary CTxOut from CUtxoEntry.
|
|
// Only used by AreInputsStandard which needs a CTxOut reference.
|
|
(void)input;
|
|
(void)inputs;
|
|
throw std::runtime_error("CTransaction::GetOutputFor() : use UTXO entries directly");
|
|
}
|
|
|
|
int64_t CTransaction::GetValueIn(const MapPrevTx& inputs) const
|
|
{
|
|
if (IsCoinBase())
|
|
return 0;
|
|
|
|
int64_t nResult = 0;
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
auto mi = inputs.find(txin.prevout);
|
|
if (mi == inputs.end())
|
|
throw std::runtime_error("CTransaction::GetValueIn() : input not found");
|
|
nResult += mi->second.nValue;
|
|
}
|
|
return nResult;
|
|
}
|
|
|
|
unsigned int CTransaction::GetP2SHSigOpCount(const MapPrevTx& inputs) const
|
|
{
|
|
if (IsCoinBase())
|
|
return 0;
|
|
|
|
unsigned int nSigOps = 0;
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
auto mi = inputs.find(txin.prevout);
|
|
if (mi == inputs.end())
|
|
continue;
|
|
const CScript& scriptPubKey = mi->second.scriptPubKey;
|
|
if (scriptPubKey.IsPayToScriptHash())
|
|
nSigOps += scriptPubKey.GetSigOpCount(txin.scriptSig);
|
|
}
|
|
return nSigOps;
|
|
}
|
|
|
|
bool CTransaction::ConnectInputs(CTxDBBase& txdb, const MapPrevTx& inputs,
|
|
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner,
|
|
std::vector<CScriptCheck>* pvChecks)
|
|
{
|
|
// Validate inputs against UTXO entries and verify signatures.
|
|
// Double-spend is impossible here: FetchInputs only returns entries that exist
|
|
// in the UTXO DB (unspent) or mapPendingUtxos (created earlier in this block).
|
|
if (!IsCoinBase())
|
|
{
|
|
int64_t nValueIn = 0;
|
|
int64_t nFees = 0;
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
COutPoint prevout = txin.prevout;
|
|
auto mi = inputs.find(prevout);
|
|
if (mi == inputs.end())
|
|
return DoS(100, error("ConnectInputs() : %s input %s:%d not found", GetHash().ToString().substr(0,10).c_str(), prevout.hash.ToString().substr(0,10).c_str(), prevout.n));
|
|
const CUtxoEntry& entry = mi->second;
|
|
|
|
if (entry.fCoinBase || entry.fCoinStake)
|
|
{
|
|
if (pindexBlock->nHeight - entry.nHeight < nCoinbaseMaturity)
|
|
return error("ConnectInputs() : tried to spend %s at depth %d", entry.fCoinBase ? "coinbase" : "coinstake", pindexBlock->nHeight - entry.nHeight);
|
|
}
|
|
|
|
if (entry.nTxTime > nTime)
|
|
return DoS(100, error("ConnectInputs() : transaction timestamp earlier than input transaction"));
|
|
|
|
nValueIn += entry.nValue;
|
|
if (!MoneyRange(entry.nValue) || !MoneyRange(nValueIn))
|
|
return DoS(100, error("ConnectInputs() : txin values out of range"));
|
|
}
|
|
|
|
// The first loop above does all the inexpensive checks.
|
|
// Only if ALL inputs pass do we perform expensive ECDSA signature checks.
|
|
// Helps prevent CPU exhaustion attacks.
|
|
const uint256 hashTx = GetHash();
|
|
for (unsigned int i = 0; i < vin.size(); i++)
|
|
{
|
|
COutPoint prevout = vin[i].prevout;
|
|
const CUtxoEntry& entry = inputs.find(prevout)->second;
|
|
|
|
// Skip ECDSA signature verification when connecting blocks (fBlock=true)
|
|
// before the last blockchain checkpoint. This is safe because block merkle hashes are
|
|
// still computed and checked, and any change will be caught at the next checkpoint.
|
|
if (!(fBlock && (nBestHeight < Checkpoints::GetTotalBlocksEstimate())))
|
|
{
|
|
// Check signature cache: skip re-verification for scripts already
|
|
// validated during mempool acceptance or prior block connections.
|
|
if (scriptVerifyCache.Get(hashTx, i))
|
|
continue;
|
|
|
|
if (pvChecks)
|
|
{
|
|
pvChecks->push_back(CScriptCheck(entry.scriptPubKey, vin[i].scriptSig, *this, i, 0));
|
|
}
|
|
else
|
|
{
|
|
// Verify signature using scriptPubKey from UTXO entry
|
|
if (!VerifyScript(vin[i].scriptSig, entry.scriptPubKey, *this, i, 0))
|
|
return DoS(100, error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,10).c_str()));
|
|
scriptVerifyCache.Set(hashTx, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!IsCoinStake())
|
|
{
|
|
if (nValueIn < GetValueOut())
|
|
return DoS(100, error("ConnectInputs() : %s value in < value out", GetHash().ToString().substr(0,10).c_str()));
|
|
|
|
// Tally transaction fees
|
|
int64_t nTxFee = nValueIn - GetValueOut();
|
|
if (nTxFee < 0)
|
|
return DoS(100, error("ConnectInputs() : %s nTxFee < 0", GetHash().ToString().substr(0,10).c_str()));
|
|
|
|
// triangles: enforce transaction fees for every block
|
|
if (nTxFee < GetMinFee())
|
|
return fBlock? DoS(100, error("ConnectInputs() : %s not paying required fee=%s, paid=%s", GetHash().ToString().substr(0,10).c_str(), FormatMoney(GetMinFee()).c_str(), FormatMoney(nTxFee).c_str())) : false;
|
|
|
|
nFees += nTxFee;
|
|
if (!MoneyRange(nFees))
|
|
return DoS(100, error("ConnectInputs() : nFees out of range"));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool CTransaction::ClientConnectInputs()
|
|
{
|
|
if (IsCoinBase())
|
|
return false;
|
|
|
|
// Take over previous transactions' spent pointers
|
|
{
|
|
LOCK(mempool.cs);
|
|
int64_t nValueIn = 0;
|
|
for (unsigned int i = 0; i < vin.size(); i++)
|
|
{
|
|
// Get prev tx from single transactions in memory
|
|
COutPoint prevout = vin[i].prevout;
|
|
if (!mempool.exists(prevout.hash))
|
|
return false;
|
|
CTransaction& txPrev = mempool.lookup(prevout.hash);
|
|
|
|
if (prevout.n >= txPrev.vout.size())
|
|
return false;
|
|
|
|
// Verify signature
|
|
if (!VerifySignature(txPrev, *this, i, 0))
|
|
return error("ConnectInputs() : VerifySignature failed");
|
|
|
|
///// this is redundant with the mempool.mapNextTx stuff,
|
|
///// not sure which I want to get rid of
|
|
///// this has to go away now that posNext is gone
|
|
// // Check for conflicts
|
|
// if (!txPrev.vout[prevout.n].posNext.IsNull())
|
|
// return error("ConnectInputs() : prev tx already used");
|
|
//
|
|
// // Flag outpoints as used
|
|
// txPrev.vout[prevout.n].posNext = posThisTx;
|
|
|
|
nValueIn += txPrev.vout[prevout.n].nValue;
|
|
|
|
if (!MoneyRange(txPrev.vout[prevout.n].nValue) || !MoneyRange(nValueIn))
|
|
return error("ClientConnectInputs() : txin values out of range");
|
|
}
|
|
if (GetValueOut() > nValueIn)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Extract address type and hash160 from a script for address indexing.
|
|
* Returns true if the script is a supported type (P2PKH or P2SH).
|
|
*/
|
|
static bool GetAddressFromScript(const CScript& script, int& nType, uint160& hashBytes)
|
|
{
|
|
CTxDestination dest;
|
|
if (!ExtractDestination(script, dest))
|
|
return false;
|
|
|
|
const CKeyID* keyId = std::get_if<CKeyID>(&dest);
|
|
if (keyId) {
|
|
nType = ADDR_TYPE_P2PKH;
|
|
hashBytes = *keyId;
|
|
return true;
|
|
}
|
|
|
|
const CScriptID* scriptId = std::get_if<CScriptID>(&dest);
|
|
if (scriptId) {
|
|
nType = ADDR_TYPE_P2SH;
|
|
hashBytes = *scriptId;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CBlock::DisconnectBlock(CTxDBBase& txdb, CBlockIndex* pindex)
|
|
{
|
|
// Disconnect in reverse order
|
|
for (int i = vtx.size()-1; i >= 0; i--)
|
|
if (!vtx[i].DisconnectInputs(txdb))
|
|
return false;
|
|
|
|
// Undo UTXO entries for this block (reverse of ConnectBlock's UTXO writes)
|
|
for (int i = (int)vtx.size()-1; i >= 0; i--)
|
|
{
|
|
const CTransaction& tx = vtx[i];
|
|
uint256 txhash = tx.GetHash();
|
|
|
|
// Erase outputs this block created
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
if (!tx.vout[k].IsEmpty())
|
|
txdb.EraseUtxo(txhash, k);
|
|
}
|
|
|
|
// Restore inputs this block spent (read prev tx from disk to rebuild UTXO entry)
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
for (const CTxIn& txin : tx.vin)
|
|
{
|
|
CTransaction txPrev;
|
|
CTxIndex txindex;
|
|
if (txdb.ReadDiskTx(txin.prevout.hash, txPrev, txindex))
|
|
{
|
|
if (txin.prevout.n < txPrev.vout.size())
|
|
{
|
|
const CTxOut& prevout = txPrev.vout[txin.prevout.n];
|
|
CUtxoEntry utxo;
|
|
utxo.nValue = prevout.nValue;
|
|
utxo.scriptPubKey = prevout.scriptPubKey;
|
|
utxo.fCoinBase = txPrev.IsCoinBase();
|
|
utxo.fCoinStake = txPrev.IsCoinStake();
|
|
utxo.nTxTime = txPrev.nTime;
|
|
|
|
// Reconstruct exact height via block index lookup.
|
|
// Falls back to 0 if mapBlockIndex doesn't have the
|
|
// tx's block yet (safe — ConnectInputs maturity
|
|
// check then requires COINBASE_MATURITY confirmations).
|
|
utxo.nHeight = 0;
|
|
CBlock blockHeader;
|
|
if (blockHeader.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
|
|
{
|
|
auto bmi = mapBlockIndex.find(blockHeader.GetHash());
|
|
if (bmi != mapBlockIndex.end())
|
|
utxo.nHeight = bmi->second->nHeight;
|
|
}
|
|
|
|
txdb.WriteUtxo(txin.prevout.hash, txin.prevout.n, utxo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Undo address index entries for this block
|
|
if (fAddressIndex)
|
|
{
|
|
for (int i = (int)vtx.size()-1; i >= 0; i--)
|
|
{
|
|
const CTransaction& tx = vtx[i];
|
|
uint256 txhash = tx.GetHash();
|
|
|
|
// Undo outputs (remove UTXOs, subtract from balance)
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
const CTxOut& txout = tx.vout[k];
|
|
int nType;
|
|
uint160 hashBytes;
|
|
if (GetAddressFromScript(txout.scriptPubKey, nType, hashBytes))
|
|
{
|
|
txdb.EraseAddressUtxo(nType, hashBytes, txhash, k);
|
|
int64_t nBalance = 0;
|
|
txdb.ReadAddressBalance(nType, hashBytes, nBalance);
|
|
nBalance -= txout.nValue;
|
|
txdb.WriteAddressBalance(nType, hashBytes, nBalance);
|
|
txdb.EraseAddressTxId(nType, hashBytes, pindex->nHeight, i, txhash);
|
|
}
|
|
}
|
|
|
|
// Undo inputs (re-add spent UTXOs, add back to balance)
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
for (unsigned int j = 0; j < tx.vin.size(); j++)
|
|
{
|
|
const CTxIn& txin = tx.vin[j];
|
|
CTransaction txPrev;
|
|
CTxIndex txindex;
|
|
if (txdb.ReadDiskTx(txin.prevout.hash, txPrev, txindex))
|
|
{
|
|
if (txin.prevout.n < txPrev.vout.size())
|
|
{
|
|
const CTxOut& prevout = txPrev.vout[txin.prevout.n];
|
|
int nType;
|
|
uint160 hashBytes;
|
|
if (GetAddressFromScript(prevout.scriptPubKey, nType, hashBytes))
|
|
{
|
|
// Re-add the UTXO that was spent
|
|
// Find the height of the prev tx block
|
|
int nPrevHeight = 0;
|
|
if (txindex.pos.nBlockPos > 0)
|
|
{
|
|
CBlock blockPrev;
|
|
// Use a rough estimate - look up via block index
|
|
// The exact height isn't critical for the UTXO entry
|
|
nPrevHeight = pindex->nHeight; // approximation
|
|
}
|
|
txdb.WriteAddressUtxo(nType, hashBytes, txin.prevout.hash, txin.prevout.n,
|
|
prevout.nValue, nPrevHeight, prevout.scriptPubKey);
|
|
int64_t nBalance = 0;
|
|
txdb.ReadAddressBalance(nType, hashBytes, nBalance);
|
|
nBalance += prevout.nValue;
|
|
txdb.WriteAddressBalance(nType, hashBytes, nBalance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update block index on disk without changing it in memory.
|
|
// The memory index structure will be changed after the db commits.
|
|
if (pindex->pprev)
|
|
{
|
|
CDiskBlockIndex blockindexPrev(pindex->pprev);
|
|
blockindexPrev.hashNext = 0;
|
|
if (!txdb.WriteBlockIndex(blockindexPrev))
|
|
return error("DisconnectBlock() : WriteBlockIndex failed");
|
|
}
|
|
|
|
// triangles: clean up wallet after disconnecting coinstake
|
|
for (CTransaction& tx : vtx)
|
|
SyncWithWallets(tx, this, false, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CBlock::ConnectBlock(CTxDBBase& txdb, CBlockIndex* pindex, bool fJustCheck)
|
|
{
|
|
// Check it again in case a previous version let a bad block in, but skip BlockSig checking
|
|
if (!CheckBlock(!fJustCheck, !fJustCheck, false))
|
|
return false;
|
|
|
|
// Determine if this block is covered by the hardcoded checkpoint or
|
|
// our rolling assumeValid threshold. Below either: skip all input
|
|
// validation, FetchInputs, ConnectInputs, and wallet sync. Trust
|
|
// comes from either the static checkpoint map (compile-time, signed
|
|
// hashes baked into the binary) OR our own prior validation history
|
|
// (nAssumeValidThreshold, advanced after each successful connect).
|
|
//
|
|
// For the rolling threshold: only the last ASSUME_VALID_BUFFER blocks
|
|
// are fully validated every time. Everything older takes the fast
|
|
// path because we've already connected it successfully. A reorg that
|
|
// tries to rewrite within the buffer is caught by full validation.
|
|
bool fAssumeValid = IsConsensusAssumeValidHeight(pindex->nHeight);
|
|
bool fIsInitialDownload = IsInitialBlockDownload();
|
|
|
|
//// issue here: it doesn't know the version
|
|
unsigned int nTxPos;
|
|
if (fJustCheck)
|
|
// FetchInputs treats CDiskTxPos(1,1,1) as a special "refer to memorypool" indicator
|
|
// Since we're just checking the block and not actually connecting it, it might not (and probably shouldn't) be on the disk to get the transaction from
|
|
nTxPos = 1;
|
|
else
|
|
nTxPos = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION) - (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(vtx.size());
|
|
|
|
map<uint256, CTxIndex> mapQueuedChanges; // tx position index (for getrawtransaction)
|
|
MapPrevTx mapPendingUtxos; // in-block UTXO tracking
|
|
std::vector<CScriptCheck> vChecks;
|
|
CCheckQueueControl<CScriptCheck> scriptcheckcontrol(pScriptCheckQueue.get());
|
|
int64_t nFees = 0;
|
|
int64_t nValueIn = 0;
|
|
int64_t nValueOut = 0;
|
|
int64_t nStakeReward = 0;
|
|
unsigned int nSigOps = 0;
|
|
for (CTransaction& tx : vtx)
|
|
{
|
|
uint256 hashTx = tx.GetHash();
|
|
|
|
CDiskTxPos posThisTx(pindex->nFile, pindex->nBlockPos, nTxPos);
|
|
if (!fJustCheck)
|
|
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
|
|
|
// Record tx position for getrawtransaction (both fast and full paths)
|
|
mapQueuedChanges[hashTx] = CTxIndex(posThisTx, tx.vout.size());
|
|
|
|
// Fast path: below checkpoint, skip all input validation.
|
|
// Track pending UTXOs so later txs in the same block can find inputs.
|
|
if (fAssumeValid)
|
|
{
|
|
// Track money supply from input/output values
|
|
int64_t nTxValueOut = tx.GetValueOut();
|
|
nValueOut += nTxValueOut;
|
|
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
int64_t nTxValueIn = 0;
|
|
for (const CTxIn& txin : tx.vin)
|
|
{
|
|
// Check in-block pending UTXOs first, then UTXO database
|
|
auto it = mapPendingUtxos.find(txin.prevout);
|
|
if (it != mapPendingUtxos.end())
|
|
nTxValueIn += it->second.nValue;
|
|
else
|
|
{
|
|
CUtxoEntry utxo;
|
|
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
|
|
nTxValueIn += utxo.nValue;
|
|
}
|
|
}
|
|
nValueIn += nTxValueIn;
|
|
if (!tx.IsCoinStake())
|
|
nFees += nTxValueIn - nTxValueOut;
|
|
}
|
|
|
|
// Add outputs to pending UTXOs
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
if (!tx.vout[k].IsEmpty())
|
|
{
|
|
CUtxoEntry entry;
|
|
entry.nValue = tx.vout[k].nValue;
|
|
entry.nHeight = pindex->nHeight;
|
|
entry.scriptPubKey = tx.vout[k].scriptPubKey;
|
|
entry.fCoinBase = tx.IsCoinBase();
|
|
entry.fCoinStake = tx.IsCoinStake();
|
|
entry.nTxTime = tx.nTime;
|
|
mapPendingUtxos[COutPoint(hashTx, k)] = entry;
|
|
}
|
|
}
|
|
// Remove spent inputs from pending UTXOs
|
|
if (!tx.IsCoinBase())
|
|
for (const CTxIn& txin : tx.vin)
|
|
mapPendingUtxos.erase(txin.prevout);
|
|
continue;
|
|
}
|
|
|
|
// Full validation path (above checkpoint)
|
|
|
|
// BIP30: check for duplicate transaction with unspent outputs.
|
|
// With UTXO model, if any output of this txid exists in the UTXO DB, it's a duplicate.
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
if (!tx.vout[k].IsEmpty() && txdb.HaveUtxo(hashTx, k))
|
|
return false;
|
|
}
|
|
|
|
nSigOps += tx.GetLegacySigOpCount();
|
|
if (nSigOps > MAX_BLOCK_SIGOPS)
|
|
return DoS(100, error("ConnectBlock() : too many sigops"));
|
|
|
|
MapPrevTx mapInputs;
|
|
if (tx.IsCoinBase())
|
|
nValueOut += tx.GetValueOut();
|
|
else
|
|
{
|
|
bool fInvalid;
|
|
if (!tx.FetchInputs(txdb, mapPendingUtxos, true, false, mapInputs, fInvalid))
|
|
return false;
|
|
|
|
// Add in sigops done by pay-to-script-hash inputs;
|
|
// this is to prevent a "rogue miner" from creating
|
|
// an incredibly-expensive-to-validate block.
|
|
nSigOps += tx.GetP2SHSigOpCount(mapInputs);
|
|
if (nSigOps > MAX_BLOCK_SIGOPS)
|
|
return DoS(100, error("ConnectBlock() : too many sigops"));
|
|
|
|
int64_t nTxValueIn = tx.GetValueIn(mapInputs);
|
|
int64_t nTxValueOut = tx.GetValueOut();
|
|
nValueIn += nTxValueIn;
|
|
nValueOut += nTxValueOut;
|
|
if (!tx.IsCoinStake())
|
|
nFees += nTxValueIn - nTxValueOut;
|
|
if (tx.IsCoinStake())
|
|
nStakeReward = nTxValueOut - nTxValueIn;
|
|
|
|
if (!tx.ConnectInputs(txdb, mapInputs, pindex, true, false,
|
|
pScriptCheckQueue ? &vChecks : nullptr))
|
|
return false;
|
|
if (pScriptCheckQueue && vChecks.size() >= 32)
|
|
{
|
|
scriptcheckcontrol.Add(vChecks);
|
|
vChecks.clear();
|
|
}
|
|
}
|
|
|
|
// Add this tx's outputs to pending UTXOs for later txs in the block
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
if (!tx.vout[k].IsEmpty())
|
|
{
|
|
CUtxoEntry entry;
|
|
entry.nValue = tx.vout[k].nValue;
|
|
entry.nHeight = pindex->nHeight;
|
|
entry.scriptPubKey = tx.vout[k].scriptPubKey;
|
|
entry.fCoinBase = tx.IsCoinBase();
|
|
entry.fCoinStake = tx.IsCoinStake();
|
|
entry.nTxTime = tx.nTime;
|
|
mapPendingUtxos[COutPoint(hashTx, k)] = entry;
|
|
}
|
|
}
|
|
// Remove spent inputs from pending UTXOs
|
|
if (!tx.IsCoinBase())
|
|
for (const CTxIn& txin : tx.vin)
|
|
mapPendingUtxos.erase(txin.prevout);
|
|
}
|
|
|
|
if (!fAssumeValid)
|
|
{
|
|
scriptcheckcontrol.Add(vChecks);
|
|
vChecks.clear();
|
|
if (!scriptcheckcontrol.Wait())
|
|
return DoS(100, error("ConnectBlock() : script verification failed"));
|
|
|
|
if (IsProofOfWork())
|
|
{
|
|
int64_t nReward = GetProofOfWorkReward(nFees);
|
|
// Check coinbase reward
|
|
if (vtx[0].GetValueOut() > nReward)
|
|
return DoS(50, error("ConnectBlock() : coinbase reward exceeded (actual=%" PRId64 " vs calculated=%" PRId64 ")",
|
|
vtx[0].GetValueOut(),
|
|
nReward));
|
|
}
|
|
if (IsProofOfStake())
|
|
{
|
|
// triangles: coin stake tx earns reward instead of paying fee
|
|
uint64_t nCoinAge;
|
|
if (!vtx[1].GetCoinAge(txdb, nCoinAge))
|
|
return error("ConnectBlock() : %s unable to get coin age for coinstake", vtx[1].GetHash().ToString().substr(0,10).c_str());
|
|
|
|
int64_t nCalculatedStakeReward = GetProofOfStakeReward(nCoinAge, nFees);
|
|
|
|
// Enforce coinstake reward for every fully validated block.
|
|
// Historical checkpoint / rolling-assume-valid blocks take the
|
|
// fAssumeValid fast path above; stale-tip IBD must not disable
|
|
// live reward validation for blocks above that fast path.
|
|
if (!fAssumeValid)
|
|
{
|
|
if (nStakeReward > nCalculatedStakeReward)
|
|
return DoS(100, error("ConnectBlock() : coinstake pays too much(actual=%" PRId64 " vs calculated=%" PRId64 ")", nStakeReward, nCalculatedStakeReward));
|
|
}
|
|
}
|
|
}
|
|
|
|
// triangles: track money supply and mint amount info
|
|
pindex->nMint = nValueOut - nValueIn + nFees;
|
|
pindex->nMoneySupply = (pindex->pprev? pindex->pprev->nMoneySupply : 0) + nValueOut - nValueIn;
|
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
|
return error("Connect() : WriteBlockIndex for pindex failed");
|
|
|
|
if (fJustCheck)
|
|
return true;
|
|
|
|
// Write queued txindex changes
|
|
for (const auto& [hash, txindex] : mapQueuedChanges)
|
|
{
|
|
if (!txdb.UpdateTxIndex(hash, txindex))
|
|
return error("ConnectBlock() : UpdateTxIndex failed");
|
|
}
|
|
|
|
// Write UTXO database entries: add new outputs, erase spent inputs.
|
|
// Runs for both fAssumeValid (fast) and full validation paths.
|
|
for (const CTransaction& tx : vtx)
|
|
{
|
|
uint256 hashTx = tx.GetHash();
|
|
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
const CTxOut& txout = tx.vout[k];
|
|
if (txout.IsEmpty())
|
|
continue;
|
|
|
|
CUtxoEntry utxo;
|
|
utxo.nValue = txout.nValue;
|
|
utxo.nHeight = pindex->nHeight;
|
|
utxo.scriptPubKey = txout.scriptPubKey;
|
|
utxo.fCoinBase = tx.IsCoinBase();
|
|
utxo.fCoinStake = tx.IsCoinStake();
|
|
utxo.nTxTime = tx.nTime;
|
|
if (!txdb.WriteUtxo(hashTx, k, utxo))
|
|
return error("ConnectBlock() : WriteUtxo failed");
|
|
}
|
|
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
for (const CTxIn& txin : tx.vin)
|
|
{
|
|
if (!txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n))
|
|
return error("ConnectBlock() : EraseUtxo failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update address index
|
|
if (fAddressIndex)
|
|
{
|
|
// Batch balance deltas: accumulate net change per address, then
|
|
// do a single read-modify-write per unique address at the end.
|
|
// This avoids hundreds of per-output DB reads/writes per block.
|
|
std::map<std::pair<int, uint160>, int64_t> mapBalanceDeltas;
|
|
|
|
for (unsigned int i = 0; i < vtx.size(); i++)
|
|
{
|
|
const CTransaction& tx = vtx[i];
|
|
uint256 txhash = tx.GetHash();
|
|
|
|
// Index spent inputs (remove UTXOs, reduce balance)
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
for (unsigned int j = 0; j < tx.vin.size(); j++)
|
|
{
|
|
const CTxIn& txin = tx.vin[j];
|
|
bool fFoundPrevout = false;
|
|
|
|
// Check mapPendingUtxos first to avoid a DB hit for
|
|
// outputs created earlier in this same block.
|
|
auto itPending = mapPendingUtxos.find(txin.prevout);
|
|
if (itPending != mapPendingUtxos.end())
|
|
{
|
|
const CUtxoEntry& utxo = itPending->second;
|
|
int nType;
|
|
uint160 hashBytes;
|
|
if (GetAddressFromScript(utxo.scriptPubKey, nType, hashBytes))
|
|
{
|
|
txdb.EraseAddressUtxo(nType, hashBytes, txin.prevout.hash, txin.prevout.n);
|
|
mapBalanceDeltas[std::make_pair(nType, hashBytes)] -= utxo.nValue;
|
|
}
|
|
fFoundPrevout = true;
|
|
}
|
|
|
|
// Fall back to reading the full transaction from disk
|
|
if (!fFoundPrevout)
|
|
{
|
|
CTransaction txPrev;
|
|
CTxIndex txindex;
|
|
if (txdb.ReadDiskTx(txin.prevout.hash, txPrev, txindex))
|
|
{
|
|
if (txin.prevout.n < txPrev.vout.size())
|
|
{
|
|
const CTxOut& prevout = txPrev.vout[txin.prevout.n];
|
|
int nType;
|
|
uint160 hashBytes;
|
|
if (GetAddressFromScript(prevout.scriptPubKey, nType, hashBytes))
|
|
{
|
|
txdb.EraseAddressUtxo(nType, hashBytes, txin.prevout.hash, txin.prevout.n);
|
|
mapBalanceDeltas[std::make_pair(nType, hashBytes)] -= prevout.nValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Index new outputs (add UTXOs, increase balance)
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
const CTxOut& txout = tx.vout[k];
|
|
if (txout.scriptPubKey.empty() || txout.nValue == 0)
|
|
continue;
|
|
|
|
int nType;
|
|
uint160 hashBytes;
|
|
if (GetAddressFromScript(txout.scriptPubKey, nType, hashBytes))
|
|
{
|
|
// Add new UTXO
|
|
txdb.WriteAddressUtxo(nType, hashBytes, txhash, k,
|
|
txout.nValue, pindex->nHeight, txout.scriptPubKey);
|
|
// Accumulate balance increase (batched write at end)
|
|
mapBalanceDeltas[std::make_pair(nType, hashBytes)] += txout.nValue;
|
|
// Record tx in address history
|
|
txdb.WriteAddressTxId(nType, hashBytes, pindex->nHeight, i, txhash);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Batch-write all accumulated balance changes: one read + one write
|
|
// per unique address instead of per-output.
|
|
for (const auto& entry : mapBalanceDeltas)
|
|
{
|
|
if (entry.second == 0)
|
|
continue;
|
|
int64_t nBalance = 0;
|
|
txdb.ReadAddressBalance(entry.first.first, entry.first.second, nBalance);
|
|
nBalance += entry.second;
|
|
txdb.WriteAddressBalance(entry.first.first, entry.first.second, nBalance);
|
|
}
|
|
}
|
|
|
|
// Update block index on disk without changing it in memory.
|
|
// The memory index structure will be changed after the db commits.
|
|
if (pindex->pprev)
|
|
{
|
|
CDiskBlockIndex blockindexPrev(pindex->pprev);
|
|
blockindexPrev.hashNext = pindex->GetBlockHash();
|
|
if (!txdb.WriteBlockIndex(blockindexPrev))
|
|
return error("ConnectBlock() : WriteBlockIndex failed");
|
|
}
|
|
|
|
// Skip wallet sync during IBD - a full wallet rescan runs when IBD completes.
|
|
// This eliminates millions of per-transaction wallet lookups during sync.
|
|
if (!fIsInitialDownload)
|
|
{
|
|
for (CTransaction& tx : vtx)
|
|
SyncWithWallets(tx, this, true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool static Reorganize(CTxDBBase& txdb, CBlockIndex* pindexNew)
|
|
{
|
|
printf("REORGANIZE: Switching chains\n");
|
|
printf(" Old tip: %s height %d trust %s\n",
|
|
pindexBest->GetBlockHash().ToString().substr(0,20).c_str(),
|
|
pindexBest->nHeight,
|
|
CBigNum(pindexBest->nChainTrust).ToString().c_str());
|
|
printf(" New tip: %s height %d trust %s\n",
|
|
pindexNew->GetBlockHash().ToString().substr(0,20).c_str(),
|
|
pindexNew->nHeight,
|
|
CBigNum(pindexNew->nChainTrust).ToString().c_str());
|
|
|
|
// Find the fork
|
|
CBlockIndex* pfork = pindexBest;
|
|
CBlockIndex* plonger = pindexNew;
|
|
while (pfork != plonger)
|
|
{
|
|
while (plonger->nHeight > pfork->nHeight)
|
|
if (!(plonger = plonger->pprev))
|
|
return error("Reorganize() : plonger->pprev is null");
|
|
if (pfork == plonger)
|
|
break;
|
|
if (!(pfork = pfork->pprev))
|
|
return error("Reorganize() : pfork->pprev is null");
|
|
}
|
|
|
|
// Convergence rule (fix/consensus-convergence):
|
|
//
|
|
// Above the last globally shared hardened checkpoint, the valid chain
|
|
// with strictly greater cumulative chain trust wins — no depth cap,
|
|
// no local finality, no trust hysteresis.
|
|
//
|
|
// Below the hardened checkpoint: reject unconditionally. The
|
|
// checkpoint is sourced from the same compiled map (Checkpoints::
|
|
// mapCheckpoints via GetLastCheckpointHeight) on every node, so
|
|
// it is a globally shared anchor, not locally invented finality.
|
|
//
|
|
// pindexLastHardenedCheckpoint is set at startup from the same map,
|
|
// keyed by mapBlockIndex lookup of the compiled checkpoint hash. If
|
|
// that lookup fails (early IBD, reindex, or bootstrap before the
|
|
// checkpoint block has been downloaded into the local block index)
|
|
// the pointer is NULL. In that state we still know the *height* of
|
|
// the checkpoint from the compiled map directly — every node built
|
|
// from the same binary sees the same value — and we use it as the
|
|
// fail-closed floor.
|
|
//
|
|
// Boundary: reject only when pfork->nHeight < nHardenedCheckpointHeight.
|
|
// A reorg whose fork point EQUALS the checkpoint height preserves the
|
|
// checkpoint block (which both chains share) and only replaces blocks
|
|
// AFTER the checkpoint. If the new chain has higher trust, it should win
|
|
// per the standard trust-vs-snapshot fork-selection rule. Rejecting such
|
|
// a reorg would cause honest nodes that observed different height-2,172,038
|
|
// blocks to remain split forever even when they agree on the checkpoint.
|
|
int nHardenedCheckpointHeight = -1;
|
|
if (pindexLastHardenedCheckpoint)
|
|
nHardenedCheckpointHeight = pindexLastHardenedCheckpoint->nHeight;
|
|
else
|
|
nHardenedCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
|
|
if (nHardenedCheckpointHeight >= 0 && pfork->nHeight < nHardenedCheckpointHeight)
|
|
{
|
|
printf("REORGANIZE: REJECTED — fork point %d is below shared hardened checkpoint %d\n",
|
|
pfork->nHeight, nHardenedCheckpointHeight);
|
|
return error("Reorganize() : fork point %d below shared hardened checkpoint %d",
|
|
pfork->nHeight, nHardenedCheckpointHeight);
|
|
}
|
|
|
|
// List of what to disconnect
|
|
vector<CBlockIndex*> vDisconnect;
|
|
for (CBlockIndex* pindex = pindexBest; pindex != pfork; pindex = pindex->pprev)
|
|
vDisconnect.push_back(pindex);
|
|
|
|
// List of what to connect
|
|
vector<CBlockIndex*> vConnect;
|
|
for (CBlockIndex* pindex = pindexNew; pindex != pfork; pindex = pindex->pprev)
|
|
vConnect.push_back(pindex);
|
|
reverse(vConnect.begin(), vConnect.end());
|
|
|
|
printf("REORGANIZE: Fork point at height %d: %s\n",
|
|
pfork->nHeight,
|
|
pfork->GetBlockHash().ToString().substr(0,20).c_str());
|
|
printf("REORGANIZE: Disconnect %" PRIszu " blocks (heights %d..%d)\n",
|
|
vDisconnect.size(),
|
|
pfork->nHeight + 1,
|
|
pindexBest->nHeight);
|
|
printf("REORGANIZE: Connect %" PRIszu " blocks (heights %d..%d)\n",
|
|
vConnect.size(),
|
|
pfork->nHeight + 1,
|
|
pindexNew->nHeight);
|
|
|
|
// Disconnect shorter branch
|
|
vector<CTransaction> vResurrect;
|
|
for (CBlockIndex* pindex : vDisconnect)
|
|
{
|
|
CBlock block;
|
|
if (!block.ReadFromDisk(pindex))
|
|
return error("Reorganize() : ReadFromDisk for disconnect failed");
|
|
if (!block.DisconnectBlock(txdb, pindex))
|
|
return error("Reorganize() : DisconnectBlock %s failed", pindex->GetBlockHash().ToString().substr(0,20).c_str());
|
|
|
|
// Queue memory transactions to resurrect
|
|
for (const CTransaction& tx : block.vtx)
|
|
if (!(tx.IsCoinBase() || tx.IsCoinStake()))
|
|
vResurrect.push_back(tx);
|
|
}
|
|
|
|
// Connect longer branch
|
|
vector<CTransaction> vDelete;
|
|
for (CBlockIndex* pindex : vConnect)
|
|
{
|
|
CBlock block;
|
|
if (!block.ReadFromDisk(pindex))
|
|
return error("Reorganize() : ReadFromDisk for connect failed");
|
|
if (!block.ConnectBlock(txdb, pindex))
|
|
{
|
|
// Invalid block
|
|
return error("Reorganize() : ConnectBlock %s failed", pindex->GetBlockHash().ToString().substr(0,20).c_str());
|
|
}
|
|
|
|
// Queue memory transactions to delete
|
|
for (const CTransaction& tx : block.vtx)
|
|
vDelete.push_back(tx);
|
|
}
|
|
if (!txdb.WriteHashBestChain(pindexNew->GetBlockHash()))
|
|
return error("Reorganize() : WriteHashBestChain failed");
|
|
if (!UpdateAddressIndexSyncState(txdb, pindexNew))
|
|
return error("Reorganize() : WriteAddressIndexBestChain failed");
|
|
|
|
// Make sure it's successfully written to disk before changing memory structure
|
|
if (!txdb.TxnCommit())
|
|
return error("Reorganize() : TxnCommit failed");
|
|
|
|
// ======================================================================
|
|
// CRITICAL: All operations below this point must be in-memory only and
|
|
// should never fail. The DB transaction is committed, so we cannot abort.
|
|
// ======================================================================
|
|
|
|
// Disconnect shorter branch (in-memory only)
|
|
for (CBlockIndex* pindex : vDisconnect)
|
|
if (pindex->pprev)
|
|
pindex->pprev->pnext = nullptr;
|
|
|
|
// Connect longer branch (in-memory only)
|
|
for (CBlockIndex* pindex : vConnect)
|
|
if (pindex->pprev)
|
|
pindex->pprev->pnext = pindex;
|
|
|
|
// Remove disconnected PoS blocks from setStakeSeen so they don't
|
|
// block acceptance of valid blocks on the winning chain.
|
|
// This MUST happen after commit to maintain consistency.
|
|
for (CBlockIndex* pindex : vDisconnect)
|
|
if (pindex->IsProofOfStake())
|
|
setStakeSeen.erase(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
|
|
|
// Resurrect memory transactions that were in the disconnected branch
|
|
unsigned int nResurrected = 0;
|
|
for (CTransaction& tx : vResurrect)
|
|
{
|
|
if (tx.AcceptToMemoryPool(txdb, false))
|
|
nResurrected++;
|
|
}
|
|
if (nResurrected > 0)
|
|
printf("REORGANIZE: resurrected %u transactions to mempool\n", nResurrected);
|
|
|
|
// Delete redundant memory transactions that are in the connected branch
|
|
for (CTransaction& tx : vDelete) {
|
|
mempool.remove(tx);
|
|
mempool.removeConflicts(tx);
|
|
}
|
|
|
|
printf("REORGANIZE: done (fork at height %d, %zu disconnected, %zu connected)\n",
|
|
pfork->nHeight, vDisconnect.size(), vConnect.size());
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Called from inside SetBestChain: attaches a block to the new best chain being built
|
|
bool CBlock::SetBestChainInner(CTxDBBase& txdb, CBlockIndex *pindexNew)
|
|
{
|
|
uint256 hash = GetHash();
|
|
|
|
// Adding to current best branch
|
|
if (!ConnectBlock(txdb, pindexNew) || !txdb.WriteHashBestChain(hash) || !UpdateAddressIndexSyncState(txdb, pindexNew))
|
|
{
|
|
txdb.TxnAbort();
|
|
InvalidChainFound(pindexNew);
|
|
return false;
|
|
}
|
|
if (!txdb.TxnCommit())
|
|
return error("SetBestChain() : TxnCommit failed");
|
|
|
|
// Add to current best branch
|
|
pindexNew->pprev->pnext = pindexNew;
|
|
|
|
// Delete redundant memory transactions
|
|
for (CTransaction& tx : vtx)
|
|
mempool.remove(tx);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CBlock::SetBestChain(CTxDBBase& txdb, CBlockIndex* pindexNew)
|
|
{
|
|
uint256 hash = GetHash();
|
|
|
|
if (!txdb.TxnBegin())
|
|
return error("SetBestChain() : TxnBegin failed");
|
|
|
|
if (pindexGenesisBlock == nullptr && hash == (!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet))
|
|
{
|
|
txdb.WriteHashBestChain(hash);
|
|
if (!UpdateAddressIndexSyncState(txdb, pindexNew))
|
|
return error("SetBestChain() : WriteAddressIndexBestChain failed");
|
|
if (!txdb.TxnCommit())
|
|
return error("SetBestChain() : TxnCommit failed");
|
|
pindexGenesisBlock = pindexNew;
|
|
}
|
|
else if (hashPrevBlock == hashBestChain)
|
|
{
|
|
if (!SetBestChainInner(txdb, pindexNew))
|
|
return error("SetBestChain() : SetBestChainInner failed");
|
|
}
|
|
else
|
|
{
|
|
// the first block in the new chain that will cause it to become the new best chain
|
|
CBlockIndex *pindexIntermediate = pindexNew;
|
|
|
|
// list of blocks that need to be connected afterwards
|
|
std::vector<CBlockIndex*> vpindexSecondary;
|
|
|
|
// Reorganize is costly in terms of db load, as it works in a single db transaction.
|
|
// Try to limit how much needs to be done inside
|
|
while (pindexIntermediate->pprev && pindexIntermediate->pprev->nChainTrust > pindexBest->nChainTrust)
|
|
{
|
|
vpindexSecondary.push_back(pindexIntermediate);
|
|
pindexIntermediate = pindexIntermediate->pprev;
|
|
}
|
|
|
|
if (!vpindexSecondary.empty())
|
|
printf("Postponing %" PRIszu " reconnects\n", vpindexSecondary.size());
|
|
|
|
// Switch to new best branch
|
|
if (!Reorganize(txdb, pindexIntermediate))
|
|
{
|
|
txdb.TxnAbort();
|
|
InvalidChainFound(pindexNew);
|
|
return error("SetBestChain() : Reorganize failed");
|
|
}
|
|
|
|
// Connect further blocks
|
|
for (auto rit = vpindexSecondary.rbegin(); rit != vpindexSecondary.rend(); ++rit)
|
|
{
|
|
CBlockIndex *pindex = *rit;
|
|
CBlock block;
|
|
if (!block.ReadFromDisk(pindex))
|
|
{
|
|
printf("SetBestChain() : ReadFromDisk failed\n");
|
|
break;
|
|
}
|
|
if (!txdb.TxnBegin()) {
|
|
printf("SetBestChain() : TxnBegin 2 failed\n");
|
|
break;
|
|
}
|
|
// errors now are not fatal, we still did a reorganisation to a new chain in a valid way
|
|
if (!block.SetBestChainInner(txdb, pindex))
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update best block in wallet (so we can detect restored wallets).
|
|
// During IBD, skip this so the wallet knows it needs rescanning on restart.
|
|
bool fIsInitialDownload = IsInitialBlockDownload();
|
|
if (!fIsInitialDownload)
|
|
{
|
|
const CBlockLocator locator(pindexNew);
|
|
::SetBestChain(locator);
|
|
}
|
|
|
|
// New best block
|
|
hashBestChain = hash;
|
|
pindexBest = pindexNew;
|
|
pblockindexFBBHLast = nullptr;
|
|
nBestHeight = pindexBest->nHeight;
|
|
nBestChainTrust = pindexNew->nChainTrust;
|
|
nTimeBestReceived = GetTime();
|
|
nTransactionsUpdated++;
|
|
|
|
// pindexLastHardenedCheckpoint is intentionally NOT advanced here. See
|
|
// fix/consensus-convergence in init.cpp and Reorganize().
|
|
|
|
// Rolling assumeValid threshold: advance so blocks older than
|
|
// ASSUME_VALID_BUFFER from the tip take the fast path on future
|
|
// connects. We do this AFTER the finality checkpoint update so the
|
|
// fast-path boundary always lags the finality boundary by at least
|
|
// ASSUME_VALID_BUFFER — no gap, no overlap risk on reorgs.
|
|
//
|
|
// Only advance when fully synced. During IBD we want full validation
|
|
// until we're confident the chain is correct, then we can lean on
|
|
// prior validation history.
|
|
if (!IsInitialBlockDownload() && nBestHeight > (int)ASSUME_VALID_BUFFER)
|
|
{
|
|
int newThreshold = nBestHeight - (int)ASSUME_VALID_BUFFER;
|
|
if (newThreshold > nAssumeValidThreshold)
|
|
{
|
|
nAssumeValidThreshold = newThreshold;
|
|
printf("ASSUME-VALID: threshold advanced to block %d (full validation only for last %d blocks)\n",
|
|
nAssumeValidThreshold, ASSUME_VALID_BUFFER);
|
|
}
|
|
}
|
|
|
|
uint256 nBestBlockTrust = (pindexBest->nHeight != 0 && pindexBest->pprev) ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
|
|
|
// Log every 5000 blocks during sync, every block once caught up
|
|
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
|
{
|
|
static int64_t nLastLogTime = 0;
|
|
static int nLastLogHeight = 0;
|
|
int64_t nNow = GetTimeMillis();
|
|
double dRate = 0;
|
|
if (nLastLogTime > 0 && nNow > nLastLogTime)
|
|
dRate = (double)(nBestHeight - nLastLogHeight) * 1000.0 / (double)(nNow - nLastLogTime);
|
|
printf("SetBestChain: new best=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s %.1f blk/s\n",
|
|
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight,
|
|
CBigNum(nBestChainTrust).ToString().c_str(),
|
|
nBestBlockTrust.Get64(),
|
|
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str(),
|
|
dRate);
|
|
nLastLogTime = nNow;
|
|
nLastLogHeight = nBestHeight;
|
|
}
|
|
|
|
if (fDebug)
|
|
printf("Stake checkpoint: %x\n", pindexBest->nStakeModifierChecksum);
|
|
|
|
// Check the version of the last 100 blocks to see if we need to upgrade:
|
|
if (!fIsInitialDownload)
|
|
{
|
|
int nUpgraded = 0;
|
|
const CBlockIndex* pindex = pindexBest;
|
|
for (int i = 0; i < 100 && pindex != nullptr; i++)
|
|
{
|
|
if (pindex->nVersion > CBlock::CURRENT_VERSION)
|
|
++nUpgraded;
|
|
pindex = pindex->pprev;
|
|
}
|
|
if (nUpgraded > 0)
|
|
printf("SetBestChain: %d of last 100 blocks above version %d\n", nUpgraded, CBlock::CURRENT_VERSION);
|
|
if (nUpgraded > 100/2)
|
|
// strMiscWarning is read by GetWarnings(), called by Qt and the JSON-RPC code to warn the user:
|
|
strMiscWarning = _("Warning: This version is obsolete, upgrade required!");
|
|
}
|
|
|
|
std::string strCmd = GetArg(std::string_view{"-blocknotify"}, std::string_view{""});
|
|
|
|
if (!fIsInitialDownload && !strCmd.empty())
|
|
{
|
|
ReplaceAll(strCmd, "%s", hashBestChain.GetHex());
|
|
std::thread(runCommand, strCmd).detach(); // thread runs free
|
|
}
|
|
|
|
#ifdef ENABLE_ZMQ
|
|
if (!fIsInitialDownload && pzmqNotifier)
|
|
pzmqNotifier->NotifyBlockHash(hashBestChain);
|
|
#endif
|
|
|
|
// SSE notification for new block
|
|
if (!fIsInitialDownload && pNotificationQueue)
|
|
{
|
|
std::string strBlockEvent = strprintf(
|
|
"{\"type\":\"block\",\"hash\":\"%s\",\"height\":%d}",
|
|
hashBestChain.GetHex().c_str(),
|
|
pindexBest->nHeight);
|
|
pNotificationQueue->Push(strBlockEvent);
|
|
}
|
|
|
|
// Detect IBD-to-synced transition and trigger deferred work:
|
|
// wallet rescan (since SyncWithWallets was skipped) and smsg chain scan.
|
|
{
|
|
static bool fWasInitialDownload = true;
|
|
if (fWasInitialDownload && !fIsInitialDownload)
|
|
{
|
|
printf("*** Initial block download complete at height %d ***\n", nBestHeight);
|
|
|
|
// Trim orphan blocks to normal limit now that IBD is done
|
|
LimitOrphanBlocks(MAX_ORPHAN_BLOCKS);
|
|
|
|
// Update wallet best chain locator now that IBD is done
|
|
const CBlockLocator locator(pindexBest);
|
|
::SetBestChain(locator);
|
|
|
|
// Run expensive post-IBD scans in the background so reaching tip
|
|
// is not blocked by wallet/message index rebuild work.
|
|
bool fStartPostIbdWork = false;
|
|
{
|
|
LOCK(cs_PostIbdWork);
|
|
if (!fPostIbdWorkStarted)
|
|
{
|
|
fPostIbdWorkStarted = true;
|
|
fStartPostIbdWork = true;
|
|
}
|
|
}
|
|
|
|
if (fStartPostIbdWork && !NewThread(ThreadPostIbdWork, nullptr))
|
|
{
|
|
LOCK(cs_PostIbdWork);
|
|
fPostIbdWorkStarted = false;
|
|
printf("Warning: post-IBD background work thread could not be started; scans skipped.\n");
|
|
}
|
|
}
|
|
fWasInitialDownload = fIsInitialDownload;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// triangles: total coin age spent in transaction, in the unit of coin-days.
|
|
// Only those coins meeting minimum age requirement counts. As those
|
|
// transactions not in main chain are not currently indexed so we
|
|
// might not find out about their coin age. Older transactions are
|
|
// guaranteed to be in main chain by sync-checkpoint. This rule is
|
|
// introduced to help nodes establish a consistent view of the coin
|
|
// age (trust score) of competing branches.
|
|
bool CTransaction::GetCoinAge(CTxDBBase& txdb, uint64_t& nCoinAge) const
|
|
{
|
|
CBigNum bnCentSecond = 0; // coin age in the unit of cent-seconds
|
|
nCoinAge = 0;
|
|
|
|
if (IsCoinBase())
|
|
return true;
|
|
|
|
for (const CTxIn& txin : vin)
|
|
{
|
|
// Look up the UTXO entry for this input
|
|
CUtxoEntry utxo;
|
|
if (!txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
|
|
{
|
|
// Lazy fallback: try old CTxIndex path
|
|
CTxIndex txindexFallback;
|
|
if (!txdb.ReadTxIndex(txin.prevout.hash, txindexFallback))
|
|
continue;
|
|
CTransaction txPrev;
|
|
if (!txPrev.ReadFromDisk(txindexFallback.pos))
|
|
continue;
|
|
if (txin.prevout.n >= txPrev.vout.size())
|
|
continue;
|
|
|
|
utxo.nValue = txPrev.vout[txin.prevout.n].nValue;
|
|
utxo.scriptPubKey = txPrev.vout[txin.prevout.n].scriptPubKey;
|
|
utxo.fCoinBase = txPrev.IsCoinBase();
|
|
utxo.fCoinStake = txPrev.IsCoinStake();
|
|
utxo.nTxTime = txPrev.nTime;
|
|
utxo.nHeight = 0;
|
|
}
|
|
|
|
if (nTime < utxo.nTxTime)
|
|
return false; // Transaction timestamp violation
|
|
|
|
// Read block header to check min age.
|
|
// Use the tx position index to find the block file/position.
|
|
CTxIndex txindex;
|
|
if (!txdb.ReadTxIndex(txin.prevout.hash, txindex))
|
|
continue;
|
|
CBlock block;
|
|
if (!block.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
|
|
return false; // unable to read block of previous transaction
|
|
if (block.GetBlockTime() + nStakeMinAge > nTime)
|
|
continue; // only count coins meeting min age requirement
|
|
|
|
int64_t nValueIn = utxo.nValue;
|
|
bnCentSecond += CBigNum(nValueIn) * (nTime - utxo.nTxTime) / CENT;
|
|
|
|
if (fDebug && GetBoolArg("-printcoinage"))
|
|
printf("coin age nValueIn=%" PRId64 " nTimeDiff=%d bnCentSecond=%s\n", nValueIn, nTime - utxo.nTxTime, bnCentSecond.ToString().c_str());
|
|
}
|
|
|
|
CBigNum bnCoinDay = bnCentSecond * CENT / (24 * 60 * 60);
|
|
if (fDebug && GetBoolArg("-printcoinage"))
|
|
printf("coin age bnCoinDay=%s\n", bnCoinDay.ToString().c_str());
|
|
nCoinAge = bnCoinDay.getuint64();
|
|
return true;
|
|
}
|
|
|
|
// triangles: total coin age spent in block, in the unit of coin-days.
|
|
bool CBlock::GetCoinAge(uint64_t& nCoinAge) const
|
|
{
|
|
nCoinAge = 0;
|
|
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
for (const CTransaction& tx : vtx)
|
|
{
|
|
uint64_t nTxCoinAge;
|
|
if (tx.GetCoinAge(txdb, nTxCoinAge))
|
|
nCoinAge += nTxCoinAge;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (nCoinAge == 0) // block coin age minimum 1 coin-day
|
|
nCoinAge = 1;
|
|
if (fDebug && GetBoolArg("-printcoinage"))
|
|
printf("block coin age total nCoinDays=%" PRId64 "\n", nCoinAge);
|
|
return true;
|
|
}
|
|
|
|
bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const uint256& hashProofOfStake)
|
|
{
|
|
// Check for duplicate
|
|
uint256 hash = GetHash();
|
|
if (mapBlockIndex.count(hash))
|
|
return error("AddToBlockIndex() : %s already exists", hash.ToString().substr(0,20).c_str());
|
|
|
|
// Construct new block index object
|
|
CBlockIndex* pindexNew = new CBlockIndex(nFile, nBlockPos, *this);
|
|
if (!pindexNew)
|
|
return error("AddToBlockIndex() : new CBlockIndex failed");
|
|
pindexNew->phashBlock = &hash;
|
|
auto miPrev = mapBlockIndex.find(hashPrevBlock);
|
|
if (miPrev != mapBlockIndex.end())
|
|
{
|
|
pindexNew->pprev = miPrev->second;
|
|
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
|
}
|
|
|
|
// triangles: compute chain trust score
|
|
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
|
|
|
// triangles: compute stake entropy bit for stake modifier
|
|
if (!pindexNew->SetStakeEntropyBit(GetStakeEntropyBit()))
|
|
return error("AddToBlockIndex() : SetStakeEntropyBit() failed");
|
|
|
|
// triangles: record proof-of-stake hash value
|
|
pindexNew->hashProofOfStake = hashProofOfStake;
|
|
|
|
// triangles: compute stake modifier
|
|
// Skip expensive computation during initial sync for blocks far below checkpoint.
|
|
// Only compute for last 1000 blocks before checkpoint and all blocks above it.
|
|
// This is safe because PoS verification is already skipped below checkpoint.
|
|
uint64_t nStakeModifier = 0;
|
|
bool fGeneratedStakeModifier = false;
|
|
int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
|
if (pindexNew->nHeight >= nCheckpointHeight - 1000)
|
|
{
|
|
if (!ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier))
|
|
return error("AddToBlockIndex() : ComputeNextStakeModifier() failed");
|
|
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
|
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
|
if (!CheckStakeModifierCheckpoints(pindexNew->nHeight, pindexNew->nStakeModifierChecksum))
|
|
return error("AddToBlockIndex() : Rejected by stake modifier checkpoint height=%d, modifier=0x%016"PRIx64, pindexNew->nHeight, nStakeModifier);
|
|
}
|
|
else
|
|
{
|
|
// Set minimal defaults during fast import
|
|
pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0);
|
|
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
|
// Only check modifier checkpoint at genesis (the only one defined)
|
|
if (pindexNew->nHeight == 0 && !CheckStakeModifierCheckpoints(0, pindexNew->nStakeModifierChecksum))
|
|
return error("AddToBlockIndex() : Rejected by stake modifier checkpoint at genesis");
|
|
}
|
|
|
|
// Add to mapBlockIndex
|
|
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
|
|
if (pindexNew->IsProofOfStake())
|
|
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
|
pindexNew->phashBlock = &mi->first;
|
|
|
|
// Write to disk block index
|
|
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
|
if (!txdb.TxnBegin())
|
|
return false;
|
|
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
|
|
|
// New best — keep the batch open so SetBestChain can add ConnectBlock
|
|
// writes to the same transaction, cutting the per-block commit count in half.
|
|
//
|
|
// Chain selection rules:
|
|
// 1. Strictly greater trust always wins (normal case).
|
|
// Deep reorgs are further gated by a 10% trust-delta check
|
|
// inside Reorganize() to prevent long-range attacks while
|
|
// still allowing natural short-fork convergence.
|
|
// 2. Equal trust: deterministic tiebreaker with timestamp preference.
|
|
// First prefer the block with the earlier timestamp (lower nTime),
|
|
// then break remaining ties by lower hash. This converges faster
|
|
// because the earlier block is more likely to have propagated first.
|
|
// Rate-limited to one equal-trust reorg per 2 minutes.
|
|
bool fNewBest = false;
|
|
static int64_t nLastEqualTrustReorg = 0;
|
|
if (pindexNew->nChainTrust > nBestChainTrust)
|
|
fNewBest = true;
|
|
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
|
|
GetTime() - nLastEqualTrustReorg > 2 * 60)
|
|
{
|
|
// Prefer earlier timestamp, then lower hash as final tiebreaker
|
|
bool fPreferNew = false;
|
|
if (pindexNew->nTime < pindexBest->nTime)
|
|
fPreferNew = true;
|
|
else if (pindexNew->nTime == pindexBest->nTime)
|
|
fPreferNew = (pindexNew->GetBlockHash() < pindexBest->GetBlockHash());
|
|
|
|
if (fPreferNew)
|
|
{
|
|
fNewBest = true;
|
|
nLastEqualTrustReorg = GetTime();
|
|
}
|
|
}
|
|
|
|
if (fNewBest)
|
|
{
|
|
if (!SetBestChain(txdb, pindexNew))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!txdb.TxnCommit())
|
|
return false;
|
|
}
|
|
|
|
if (pindexNew == pindexBest)
|
|
{
|
|
// Notify UI to display prev block's coinbase if it was ours
|
|
static uint256 hashPrevBestCoinBase;
|
|
UpdatedTransaction(hashPrevBestCoinBase);
|
|
hashPrevBestCoinBase = vtx[0].GetHash();
|
|
}
|
|
|
|
uiInterface.NotifyBlocksChanged();
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CBlock::CheckBlock(bool fCheckPOW, bool fCheckMerkleRoot, bool fCheckSig) const
|
|
{
|
|
// These are checks that are independent of context
|
|
// that can be verified before saving an orphan block.
|
|
|
|
// Size limits
|
|
if (vtx.empty() || vtx.size() > MAX_BLOCK_SIZE || ::GetSerializeSize(*this, SER_NETWORK, PROTOCOL_VERSION) > MAX_BLOCK_SIZE)
|
|
return DoS(100, error("CheckBlock() : size limits failed"));
|
|
|
|
// Check proof of work matches claimed amount.
|
|
// Genesis block is a hardcoded trust anchor — exempt from PoW check
|
|
// (same exemption as CBlock::ReadFromDisk). All other PoW blocks
|
|
// must pass CheckProofOfWork.
|
|
if (fCheckPOW && IsProofOfWork() &&
|
|
GetHash() != hashGenesisBlockOfficial &&
|
|
GetHash() != hashGenesisBlockTestNet &&
|
|
!CheckProofOfWork(GetHash(), nBits))
|
|
return DoS(50, error("CheckBlock() : proof of work failed"));
|
|
|
|
// Check timestamp: reject blocks obviously too far in the future.
|
|
// Use a generous 15-minute window from the raw system clock.
|
|
// GetAdjustedTime() is NOT used here because it incorporates peer-reported
|
|
// time offsets that differ between Tor nodes, causing nondeterministic
|
|
// block rejection — the primary cause of persistent chain splits.
|
|
// The deterministic timestamp checks in AcceptBlock (median-time-past,
|
|
// prev-block-time with 3-min drift) still enforce tight rules.
|
|
if (GetBlockTime() > GetTime() + 15 * 60)
|
|
return error("CheckBlock() : block timestamp too far in the future");
|
|
|
|
// First transaction must be coinbase, the rest must not be
|
|
if (vtx.empty() || !vtx[0].IsCoinBase())
|
|
return DoS(100, error("CheckBlock() : first tx is not coinbase"));
|
|
for (unsigned int i = 1; i < vtx.size(); i++)
|
|
if (vtx[i].IsCoinBase())
|
|
return DoS(100, error("CheckBlock() : more than one coinbase"));
|
|
|
|
// Check coinbase timestamp
|
|
// NOTE: Must use the pre-fork (10-minute) drift tolerance here because
|
|
// CheckBlock() is context-free (no nHeight) and can be called on blocks
|
|
// at any height, including during chain reorgs from old fork chains.
|
|
// The tight 90-second drift for post-fork blocks is enforced separately
|
|
// in AcceptBlock/ConnectBlock with proper height context.
|
|
if (GetBlockTime() > (int64_t)vtx[0].nTime + 10 * 60)
|
|
return DoS(50, error("CheckBlock() : coinbase timestamp is too early"));
|
|
|
|
if (IsProofOfStake())
|
|
{
|
|
// Coinbase output should be empty if proof-of-stake block
|
|
if (vtx[0].vout.size() != 1 || !vtx[0].vout[0].IsEmpty())
|
|
return DoS(100, error("CheckBlock() : coinbase output not empty for proof-of-stake block"));
|
|
|
|
// Second transaction must be coinstake, the rest must not be
|
|
if (vtx.empty() || !vtx[1].IsCoinStake())
|
|
return DoS(100, error("CheckBlock() : second tx is not coinstake"));
|
|
for (unsigned int i = 2; i < vtx.size(); i++)
|
|
if (vtx[i].IsCoinStake())
|
|
return DoS(100, error("CheckBlock() : more than one coinstake"));
|
|
|
|
// Check coinstake timestamp
|
|
if (!CheckCoinStakeTimestamp(GetBlockTime(), (int64_t)vtx[1].nTime))
|
|
return DoS(50, error("CheckBlock() : coinstake timestamp violation nTimeBlock=%" PRId64 " nTimeTx=%u", GetBlockTime(), vtx[1].nTime));
|
|
|
|
// triangles: check proof-of-stake block signature
|
|
if (fCheckSig && !CheckBlockSignature())
|
|
return DoS(100, error("CheckBlock() : bad proof-of-stake block signature"));
|
|
}
|
|
|
|
// Check transactions
|
|
for (const CTransaction& tx : vtx)
|
|
{
|
|
if (!tx.CheckTransaction())
|
|
return DoS(tx.nDoS, error("CheckBlock() : CheckTransaction failed"));
|
|
|
|
// triangles: check transaction timestamp
|
|
if (GetBlockTime() < (int64_t)tx.nTime)
|
|
return DoS(50, error("CheckBlock() : block timestamp earlier than transaction timestamp"));
|
|
}
|
|
|
|
// Check for duplicate txids. This is caught by ConnectInputs(),
|
|
// but catching it earlier avoids a potential DoS attack:
|
|
set<uint256> uniqueTx;
|
|
for (const CTransaction& tx : vtx)
|
|
{
|
|
uniqueTx.insert(tx.GetHash());
|
|
}
|
|
if (uniqueTx.size() != vtx.size())
|
|
return DoS(100, error("CheckBlock() : duplicate transaction"));
|
|
|
|
unsigned int nSigOps = 0;
|
|
for (const CTransaction& tx : vtx)
|
|
{
|
|
nSigOps += tx.GetLegacySigOpCount();
|
|
}
|
|
if (nSigOps > MAX_BLOCK_SIGOPS)
|
|
return DoS(100, error("CheckBlock() : out-of-bounds SigOpCount"));
|
|
|
|
// Check merkle root
|
|
if (fCheckMerkleRoot && hashMerkleRoot != BuildMerkleTree())
|
|
return DoS(100, error("CheckBlock() : hashMerkleRoot mismatch"));
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CBlock::AcceptBlock()
|
|
{
|
|
// Check for duplicate
|
|
uint256 hash = GetHash();
|
|
if (mapBlockIndex.count(hash))
|
|
return error("AcceptBlock() : block already in mapBlockIndex");
|
|
|
|
// Get prev block index
|
|
auto mi = mapBlockIndex.find(hashPrevBlock);
|
|
if (mi == mapBlockIndex.end())
|
|
return DoS(10, error("AcceptBlock() : prev block not found"));
|
|
CBlockIndex* pindexPrev = mi->second;
|
|
int nHeight = pindexPrev->nHeight+1;
|
|
|
|
if (IsProofOfWork() && nHeight > CUTOFF_POW_BLOCK)
|
|
return DoS(100, error("AcceptBlock() : No proof-of-work allowed anymore (height = %d)", nHeight));
|
|
|
|
if (IsProofOfStake() && nHeight < MODIFIER_INTERVAL_SWITCH)
|
|
return DoS(100, error("AcceptBlock() : reject proof-of-stake at height %d", nHeight));
|
|
|
|
// Check proof-of-work or proof-of-stake
|
|
if (nBits != GetNextTargetRequired(pindexPrev, IsProofOfStake()))
|
|
{
|
|
if (nHeight > CRAPCHAIN_CUTOFF_BLOCK)
|
|
{
|
|
return DoS(100, error("AcceptBlock() : incorrect %s", IsProofOfWork() ? "proof-of-work" : "proof-of-stake"));
|
|
}
|
|
else
|
|
{
|
|
// blocks generated prior to Pharao release (version 4) are automatically accepted
|
|
if (fDebug)
|
|
printf("ProcessBlock(): pre-Pharao version proof-of-stake accepted for block %d\n", nHeight);
|
|
}
|
|
}
|
|
else
|
|
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(), nHeight) < pindexPrev->GetBlockTime())
|
|
return error("AcceptBlock() : block's timestamp is too early");
|
|
|
|
// Check that all transactions are finalized
|
|
for (const CTransaction& tx : vtx)
|
|
if (!tx.IsFinal(nHeight, GetBlockTime()))
|
|
return DoS(10, error("AcceptBlock() : contains a non-final transaction"));
|
|
|
|
// Check that the block chain matches the known block chain up to a checkpoint
|
|
if (!Checkpoints::CheckHardened(nHeight, hash))
|
|
return DoS(100, error("AcceptBlock() : rejected by hardened checkpoint lock-in at %d", nHeight));
|
|
|
|
// triangles: verify hash target and signature of coinstake tx
|
|
uint256 hashProofOfStake = 0, targetProofOfStake = 0;
|
|
if (IsProofOfStake())
|
|
{
|
|
// The rolling validation optimization is not a signature trust root.
|
|
// Every PoS block above the compiled checkpoint must authorize its
|
|
// exact block contents, including while the local tip is stale.
|
|
if (IsBlockSignatureRequiredAtHeight(nHeight) && !CheckBlockSignature())
|
|
return DoS(100, error("AcceptBlock() : bad proof-of-stake block signature at height %d", nHeight));
|
|
|
|
if (IsConsensusAssumeValidHeight(nHeight))
|
|
{
|
|
// Historical fast path: blocks at/below hardcoded checkpoint or
|
|
// rolling assume-valid have already been accepted by chain-level
|
|
// trust, so skip expensive PoS kernel verification there only.
|
|
// Do not key this off IsInitialBlockDownload(): stale-tip IBD is
|
|
// operational state, not permission to accept unchecked live PoS.
|
|
if (nHeight % 10000 == 0)
|
|
printf("SKIP: PoS kernel check skipped for historical fast-path block %d\n", nHeight);
|
|
hashProofOfStake = 0; targetProofOfStake = 0;
|
|
}
|
|
else
|
|
{
|
|
// Verify the PoS kernel signature normally for every live block
|
|
// above the historical fast path, even if the tip is stale enough
|
|
// for IsInitialBlockDownload() to be true.
|
|
if (!CheckProofOfStake(vtx[1], nBits, hashProofOfStake, targetProofOfStake))
|
|
return DoS(100, error("AcceptBlock() : check proof-of-stake failed for block %d", nHeight));
|
|
}
|
|
}
|
|
|
|
// Sync checkpoint enforcement is disabled:
|
|
// - Master key was removed in V5 fork, no new sync checkpoints will be broadcast
|
|
// - Hardcoded checkpoints already guarantee chain integrity
|
|
// - The persisted hashSyncCheckpoint in LevelDB blocks IBD from progressing
|
|
|
|
// Legacy Triangles blocks were created before mandatory coinbase-height enforcement.
|
|
// Do NOT enforce this rule against historical chain data during recovery/import.
|
|
static const int COINBASE_HEIGHT_ENFORCEMENT_HEIGHT = 2300000;
|
|
|
|
if (nHeight >= COINBASE_HEIGHT_ENFORCEMENT_HEIGHT)
|
|
{
|
|
CScript expect = CScript() << nHeight;
|
|
if (vtx[0].vin[0].scriptSig.size() < expect.size() ||
|
|
!std::equal(expect.begin(), expect.end(), vtx[0].vin[0].scriptSig.begin()))
|
|
return DoS(100, error("AcceptBlock() : block height mismatch in coinbase"));
|
|
}
|
|
|
|
// Write block to history file
|
|
if (!CheckDiskSpace(::GetSerializeSize(*this, SER_DISK, CLIENT_VERSION)))
|
|
return error("AcceptBlock() : out of disk space");
|
|
unsigned int nFile = -1;
|
|
unsigned int nBlockPos = 0;
|
|
if (!WriteToDisk(nFile, nBlockPos))
|
|
return error("AcceptBlock() : WriteToDisk failed");
|
|
if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake))
|
|
return error("AcceptBlock() : AddToBlockIndex failed");
|
|
|
|
// Push new tip block directly to peers that are near our tip.
|
|
// On a small Tor-only network the inv->getdata->block round-trip adds
|
|
// 1-2 seconds of latency per hop. Pushing immediately cuts propagation
|
|
// to a single hop. Uses nBestKnownHeight (updated from inv/block msgs)
|
|
// rather than nStartingHeight (static, set at connect time only).
|
|
//
|
|
// For peers with fPreferHeaders (sendheaders negotiated), send a header
|
|
// announcement — saves one round-trip vs inv->getdata->block.
|
|
if (hashBestChain == hash)
|
|
{
|
|
LOCK(cs_vNodes);
|
|
for (CNode* pnode : vNodes)
|
|
{
|
|
if (!pnode->fSuccessfullyConnected)
|
|
continue;
|
|
|
|
bool fNearTip = (pnode->nBestKnownHeight >= nBestHeight - 10) ||
|
|
(pnode->nBlocksDelivered > 0);
|
|
if (fNearTip && pnode->fSendCmpct)
|
|
{
|
|
// BIP152 compact block relay: header + prefilled coinbase/coinstake +
|
|
// short IDs for remaining txs. For typical PoS blocks (0-2 txs)
|
|
// this is the complete block — no follow-up needed.
|
|
SendCompactBlock(pnode, *this);
|
|
}
|
|
else if (fNearTip)
|
|
{
|
|
// Direct full block push to near-tip peers
|
|
pnode->PushMessage("block", *this);
|
|
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
|
|
}
|
|
else if (pnode->fPreferHeaders)
|
|
{
|
|
// Header announcement for peers that requested sendheaders.
|
|
// Construct a header-only CBlock (no transactions/signature).
|
|
CBlock hdr;
|
|
hdr.nVersion = nVersion;
|
|
hdr.hashPrevBlock = hashPrevBlock;
|
|
hdr.hashMerkleRoot = hashMerkleRoot;
|
|
hdr.nTime = nTime;
|
|
hdr.nBits = nBits;
|
|
hdr.nNonce = nNonce;
|
|
std::vector<CBlock> vHeaders(1, hdr);
|
|
pnode->PushMessage("headers", vHeaders);
|
|
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint256 CBlockIndex::GetBlockTrust() const
|
|
{
|
|
CBigNum bnTarget;
|
|
bnTarget.SetCompact(nBits);
|
|
|
|
if (bnTarget <= 0)
|
|
return 0;
|
|
|
|
return ((CBigNum(1)<<256) / (bnTarget+1)).getuint256();
|
|
}
|
|
|
|
bool CBlockIndex::IsSuperMajority(int minVersion, const CBlockIndex* pstart, unsigned int nRequired, unsigned int nToCheck)
|
|
{
|
|
unsigned int nFound = 0;
|
|
for (unsigned int i = 0; i < nToCheck && nFound < nRequired && pstart != nullptr; i++)
|
|
{
|
|
if (pstart->nVersion >= minVersion)
|
|
++nFound;
|
|
pstart = pstart->pprev;
|
|
}
|
|
return (nFound >= nRequired);
|
|
}
|
|
|
|
bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
|
{
|
|
// Check for duplicate
|
|
uint256 hash = pblock->GetHash();
|
|
if (mapBlockIndex.count(hash))
|
|
return error("ProcessBlock() : already have block %d %s", mapBlockIndex[hash]->nHeight, hash.ToString().substr(0,20).c_str());
|
|
if (mapOrphanBlocks.count(hash))
|
|
return error("ProcessBlock() : already have block (orphan) %s", hash.ToString().substr(0,20).c_str());
|
|
|
|
// triangles: check proof-of-stake
|
|
// Limited duplicity on stake: prevents block flood attack
|
|
// Duplicate stake allowed only when there is orphan child block
|
|
if (pblock->IsProofOfStake() && !GetBoolArg("-ignoredupstake", false) && setStakeSeen.count(pblock->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash))
|
|
return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for block %s", pblock->GetProofOfStake().first.ToString().c_str(), pblock->GetProofOfStake().second, hash.ToString().c_str());
|
|
|
|
// Operational IBD state is never permission to skip a live proof-of-stake
|
|
// block signature. Only a candidate height committed by the latest
|
|
// hardened checkpoint uses the historical fast path.
|
|
bool checkBlockSignature = true;
|
|
const auto prevIt = mapBlockIndex.find(pblock->hashPrevBlock);
|
|
if (prevIt != mapBlockIndex.end()) {
|
|
const int candidateHeight = prevIt->second->nHeight + 1;
|
|
checkBlockSignature = IsBlockSignatureRequiredAtHeight(candidateHeight);
|
|
}
|
|
if (!pblock->CheckBlock(true, true, checkBlockSignature))
|
|
{
|
|
printf("IBD-DIAG: CheckBlock FAILED for %s (PoS=%d, IBD=%d)\n",
|
|
hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload());
|
|
return error("ProcessBlock() : CheckBlock FAILED");
|
|
}
|
|
|
|
// Anti-spam: reject blocks with insufficient difficulty to prevent memory flooding.
|
|
// Use the most recent hardened checkpoint we know about; fall back to the chain tip.
|
|
CBlockIndex* pcheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
|
if (!pcheckpoint)
|
|
pcheckpoint = pindexBest;
|
|
|
|
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain)
|
|
{
|
|
int64_t deltaTime = pblock->GetBlockTime() - pcheckpoint->nTime;
|
|
CBigNum bnNewBlock;
|
|
bnNewBlock.SetCompact(pblock->nBits);
|
|
CBigNum bnRequired;
|
|
|
|
if (pblock->IsProofOfStake())
|
|
{
|
|
const CBlockIndex* pindexLastPos = GetLastBlockIndex(pcheckpoint, true);
|
|
if (pindexLastPos)
|
|
bnRequired.SetCompact(ComputeMinStake(pindexLastPos->nBits, deltaTime, pblock->nTime));
|
|
// else: no PoS history yet (below block 9001), skip — AcceptBlock rejects PoS below MODIFIER_INTERVAL_SWITCH
|
|
}
|
|
else
|
|
{
|
|
const CBlockIndex* pindexLastPow = GetLastBlockIndex(pcheckpoint, false);
|
|
if (pindexLastPow)
|
|
bnRequired.SetCompact(ComputeMinWork(pindexLastPow->nBits, deltaTime));
|
|
}
|
|
|
|
// Anti-spam: reject blocks whose target exceeds the required minimum (i.e. blocks
|
|
// with less difficulty than required for the elapsed time-since-checkpoint).
|
|
// bnNewBlock is the candidate's compact-bits target; bnRequired is the minimum
|
|
// target for the elapsed time. In Bitcoin/PoS, a LARGER target means EASIER
|
|
// difficulty. So: bnNewBlock > bnRequired => block is easier than required =>
|
|
// "too little proof-of-stake/work" => reject.
|
|
//
|
|
// The 2026-06-30 commit cbb189a inverted this to bnNewBlock < bnRequired which
|
|
// rejected blocks that are HARDER than required (good blocks!) — verified by
|
|
// DNS3 stalling at snapshot height 2,214,547 because every canonical post-snapshot
|
|
// block was being rejected as "too little proof-of-stake". This restores the
|
|
// correct comparison and keeps the soft Misbehaving(5) score from cbb189a.
|
|
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(5);
|
|
return error("ProcessBlock() : block with too little %s", pblock->IsProofOfStake()? "proof-of-stake" : "proof-of-work");
|
|
}
|
|
}
|
|
|
|
// If don't already have its previous block, shunt it off to holding area until we get it
|
|
if (!mapBlockIndex.count(pblock->hashPrevBlock))
|
|
{
|
|
if (fDebug)
|
|
printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,20).c_str());
|
|
std::unique_ptr<CBlock> pblock2 = std::make_unique<CBlock>(*pblock);
|
|
// triangles: check proof-of-stake
|
|
if (pblock2->IsProofOfStake())
|
|
{
|
|
// Limited duplicity on stake: prevents block flood attack
|
|
// Duplicate stake allowed only when there is orphan child block
|
|
if (!GetBoolArg("-ignoredupstake", false) && setStakeSeenOrphan.count(pblock2->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash))
|
|
return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for orphan block %s", pblock2->GetProofOfStake().first.ToString().c_str(), pblock2->GetProofOfStake().second, hash.ToString().c_str());
|
|
else
|
|
setStakeSeenOrphan.insert(pblock2->GetProofOfStake());
|
|
}
|
|
uint256 hashPrevOrphan = pblock2->hashPrevBlock;
|
|
CBlock* pblock2raw = pblock2.get();
|
|
mapOrphanBlocks.insert(make_pair(hash, std::move(pblock2)));
|
|
mapOrphanBlocksByPrev.insert(make_pair(hashPrevOrphan, pblock2raw));
|
|
dequeOrphanOrder.push_back(hash); // track insertion order for FIFO eviction
|
|
|
|
// Limit orphan blocks to prevent memory exhaustion.
|
|
// Allow more orphans during IBD so out-of-order blocks from parallel
|
|
// downloads don't get evicted and re-requested.
|
|
unsigned int nMaxOrphans = IsInitialBlockDownload() ? MAX_ORPHAN_BLOCKS_IBD : MAX_ORPHAN_BLOCKS;
|
|
LimitOrphanBlocks(nMaxOrphans);
|
|
|
|
// Ask this guy to fill in what we're missing
|
|
if (pfrom && pindexBest)
|
|
{
|
|
pfrom->PushGetBlocks(pindexBest, GetOrphanRoot(pblock2raw));
|
|
// triangles: getblocks may not obtain the ancestor block rejected
|
|
// earlier by duplicate-stake check so we ask for it again directly
|
|
if (!IsInitialBlockDownload())
|
|
pfrom->AskFor(CInv(MSG_BLOCK, WantedByOrphan(pblock2raw)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Store to disk
|
|
if (!pblock->AcceptBlock())
|
|
return error("ProcessBlock() : AcceptBlock FAILED");
|
|
|
|
g_syncManager.BlockAccepted(hash);
|
|
|
|
// Recursively process any orphan blocks that depended on this one
|
|
vector<uint256> vWorkQueue;
|
|
vWorkQueue.push_back(hash);
|
|
for (unsigned int i = 0; i < vWorkQueue.size(); i++)
|
|
{
|
|
uint256 hashPrev = vWorkQueue[i];
|
|
for (auto mi = mapOrphanBlocksByPrev.lower_bound(hashPrev);
|
|
mi != mapOrphanBlocksByPrev.upper_bound(hashPrev);
|
|
++mi)
|
|
{
|
|
CBlock* pblockOrphan = mi->second;
|
|
if (pblockOrphan->AcceptBlock())
|
|
{
|
|
vWorkQueue.push_back(pblockOrphan->GetHash());
|
|
g_syncManager.BlockAccepted(pblockOrphan->GetHash());
|
|
}
|
|
mapOrphanBlocks.erase(pblockOrphan->GetHash());
|
|
setStakeSeenOrphan.erase(pblockOrphan->GetProofOfStake());
|
|
}
|
|
mapOrphanBlocksByPrev.erase(hashPrev);
|
|
}
|
|
|
|
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
|
printf("ProcessBlock: ACCEPTED block %d\n", nBestHeight);
|
|
|
|
if (IsInitialBlockDownload())
|
|
{
|
|
const unsigned int nQueued =
|
|
(g_syncManager.GetBestHeader() != 0) ? g_syncManager.QueueBlocksParallel() : 0;
|
|
if (nQueued > 0 && fDebug)
|
|
printf("IBD-DIAG: queued %u more blocks from header planner after accepting %s\n",
|
|
nQueued, hash.ToString().substr(0,20).c_str());
|
|
|
|
const unsigned int nPlannerDepth = g_syncManager.GetPlannerDepth();
|
|
if (nPlannerDepth <= CSyncManager::HEADER_SYNC_LOW_WATER)
|
|
{
|
|
const unsigned int nRefilled = g_syncManager.RequestRefillAllPeers(
|
|
g_syncManager.GetBestHeader(), CSyncManager::HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS,
|
|
(nPlannerDepth == 0) ? "post-accept planner empty" : "post-accept planner low-water");
|
|
if (nRefilled > 0 && fDebug)
|
|
printf("IBD-DIAG: post-accept requested headers from %u peers at plannerDepth=%u after block %s\n",
|
|
nRefilled, nPlannerDepth, hash.ToString().substr(0,20).c_str());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// triangles: sign block
|
|
bool CBlock::SignBlock(CWallet& wallet, int64_t nFees)
|
|
{
|
|
// if we are trying to sign
|
|
// something except proof-of-stake block template
|
|
if (!vtx[0].vout[0].IsEmpty())
|
|
return false;
|
|
|
|
// if we are trying to sign
|
|
// a complete proof-of-stake block
|
|
if (IsProofOfStake())
|
|
return true;
|
|
|
|
static int64_t nLastCoinStakeSearchTime = GetAdjustedTime(); // only initialized at startup
|
|
|
|
CKey key;
|
|
CTransaction txCoinStake;
|
|
int64_t nSearchTime = txCoinStake.nTime; // search to current time
|
|
|
|
if (nSearchTime > nLastCoinStakeSearchTime)
|
|
{
|
|
if (wallet.CreateCoinStake(wallet, nBits, nSearchTime-nLastCoinStakeSearchTime, nFees, txCoinStake, key))
|
|
{
|
|
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(), pindexBest->nHeight + 1));
|
|
|
|
// we have to make sure that we have no future timestamps in
|
|
// our transactions set
|
|
for (vector<CTransaction>::iterator it = vtx.begin(); it != vtx.end();)
|
|
if (it->nTime > nTime) { it = vtx.erase(it); } else { ++it; }
|
|
|
|
vtx.insert(vtx.begin() + 1, txCoinStake);
|
|
hashMerkleRoot = BuildMerkleTree();
|
|
|
|
// append a signature to our block
|
|
return key.Sign(GetHash(), vchBlockSig);
|
|
}
|
|
}
|
|
nLastCoinStakeSearchInterval = nSearchTime - nLastCoinStakeSearchTime;
|
|
nLastCoinStakeSearchTime = nSearchTime;
|
|
}
|
|
|
|
//printf("Sign failed\n");
|
|
return false;
|
|
}
|
|
|
|
// triangles: check block signature
|
|
bool CBlock::CheckBlockSignature() const
|
|
{
|
|
if (IsProofOfWork())
|
|
return vchBlockSig.empty();
|
|
|
|
vector<valtype> vSolutions;
|
|
TxnOutType whichType;
|
|
|
|
const CTxOut& txout = vtx[1].vout[1];
|
|
|
|
if (!Solver(txout.scriptPubKey, whichType, vSolutions))
|
|
return false;
|
|
|
|
if (whichType == TxnOutType::PubKey)
|
|
{
|
|
valtype& vchPubKey = vSolutions[0];
|
|
CKey key;
|
|
if (!key.SetPubKey(vchPubKey))
|
|
return false;
|
|
if (vchBlockSig.empty())
|
|
return false;
|
|
return key.Verify(GetHash(), vchBlockSig);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CheckDiskSpace(uint64_t nAdditionalBytes)
|
|
{
|
|
uint64_t nFreeBytesAvailable = fs::space(GetDataDir()).available;
|
|
|
|
// Check for nMinDiskSpace bytes (currently 50MB)
|
|
if (nFreeBytesAvailable < nMinDiskSpace + nAdditionalBytes)
|
|
{
|
|
fShutdown = true;
|
|
string strMessage = _("Warning: Disk space is low!");
|
|
strMiscWarning = strMessage;
|
|
printf("*** %s\n", strMessage.c_str());
|
|
uiInterface.ThreadSafeMessageBox(strMessage, "Triangles", CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
|
|
MarkShutdownFailure();
|
|
StartShutdown();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static fs::path BlockFilePath(unsigned int nFile)
|
|
{
|
|
string strBlockFn = strprintf("blk%04u.dat", nFile);
|
|
return GetDataDir() / strBlockFn;
|
|
}
|
|
|
|
FILE* OpenBlockFile(unsigned int nFile, unsigned int nBlockPos, const char* pszMode)
|
|
{
|
|
if ((nFile < 1) || (nFile == (unsigned int) -1))
|
|
return nullptr;
|
|
FILE* file = fopen(BlockFilePath(nFile).string().c_str(), pszMode);
|
|
if (!file)
|
|
return nullptr;
|
|
if (nBlockPos != 0 && !strchr(pszMode, 'a') && !strchr(pszMode, 'w'))
|
|
{
|
|
if (fseek(file, nBlockPos, SEEK_SET) != 0)
|
|
{
|
|
fclose(file);
|
|
return nullptr;
|
|
}
|
|
}
|
|
return file;
|
|
}
|
|
|
|
static unsigned int nCurrentBlockFile = 1;
|
|
|
|
FILE* AppendBlockFile(unsigned int& nFileRet)
|
|
{
|
|
nFileRet = 0;
|
|
while (true)
|
|
{
|
|
FILE* file = OpenBlockFile(nCurrentBlockFile, 0, "ab");
|
|
if (!file)
|
|
return nullptr;
|
|
if (fseek(file, 0, SEEK_END) != 0)
|
|
return nullptr;
|
|
// FAT32 file size max 4GB, fseek and ftell max 2GB, so we must stay under 2GB
|
|
if (ftell(file) < (long)(0x7F000000 - MAX_SIZE))
|
|
{
|
|
nFileRet = nCurrentBlockFile;
|
|
return file;
|
|
}
|
|
fclose(file);
|
|
nCurrentBlockFile++;
|
|
}
|
|
}
|
|
|
|
bool LoadBlockIndex(bool fAllowNew)
|
|
{
|
|
//CBigNum bnTrustedModulus;
|
|
|
|
if (fTestNet)
|
|
{
|
|
pchMessageStart[0] = 0x6f;
|
|
pchMessageStart[1] = 0x3e;
|
|
pchMessageStart[2] = 0x04;
|
|
pchMessageStart[3] = 0x13;
|
|
|
|
bnProofOfStakeLimit = bnProofOfStakeLimitTestNet; // 0x00000fff PoS base target is fixed in testnet
|
|
bnProofOfWorkLimit = bnProofOfWorkLimitTestNet; // 0x0000ffff PoW base target is fixed in testnet
|
|
nStakeMinAge = 10 * 60; // test net min age is 10 min
|
|
nStakeMaxAge = 30 * 60; // test net min age is 30 min
|
|
nModifierInterval = 60; // test modifier interval is 1 minutes
|
|
nCoinbaseMaturity = 10; // test maturity is 10 blocks
|
|
nTargetSpacing = 1 * 60; // test block spacing is 1 minutes
|
|
}
|
|
|
|
//
|
|
// Load block index
|
|
//
|
|
auto txdb_holder = MakeChainDB("cr+"); CTxDBBase& txdb = *txdb_holder;
|
|
if (!txdb.LoadBlockIndex())
|
|
return false;
|
|
|
|
//
|
|
// Init with genesis block
|
|
//
|
|
if (mapBlockIndex.empty())
|
|
{
|
|
if (!fAllowNew)
|
|
return false;
|
|
|
|
// Genesis block
|
|
|
|
const char* pszTimestamp = "july 16 2014, I'm deh besht mang, I deeed et!";
|
|
CTransaction txNew;
|
|
txNew.nTime = nChainStartTime;
|
|
txNew.vin.resize(1);
|
|
txNew.vout.resize(1);
|
|
txNew.vin[0].scriptSig = CScript() << 486604799 << CBigNum(9999) << vector<unsigned char>((const unsigned char*)pszTimestamp, (const unsigned char*)pszTimestamp + strlen(pszTimestamp));
|
|
txNew.vout[0].SetEmpty();
|
|
CBlock block;
|
|
block.vtx.push_back(txNew);
|
|
block.hashPrevBlock = 0;
|
|
block.hashMerkleRoot = block.BuildMerkleTree();
|
|
block.nVersion = 1;
|
|
block.nTime = 1405500418;
|
|
block.nBits = bnProofOfWorkLimit.GetCompact();
|
|
block.nNonce = 43;
|
|
|
|
|
|
|
|
|
|
|
|
if (false && (block.GetHash() != hashGenesisBlockOfficial)) {
|
|
|
|
// This will figure out a valid hash and Nonce if you're
|
|
// creating a different genesis block:
|
|
uint256 hashTarget = CBigNum().SetCompact(block.nBits).getuint256();
|
|
while (block.GetHash() > hashTarget)
|
|
{
|
|
++block.nNonce;
|
|
if (block.nNonce == 0)
|
|
{
|
|
printf("NONCE WRAPPED, incrementing time");
|
|
++block.nTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
//// debug print
|
|
block.print();
|
|
printf("block.GetHash() == %s\n", block.GetHash().ToString().c_str());
|
|
printf("block.hashMerkleRoot == %s\n", block.hashMerkleRoot.ToString().c_str());
|
|
printf("block.nTime = %u \n", block.nTime);
|
|
printf("block.nNonce = %u \n", block.nNonce);
|
|
|
|
assert(block.hashMerkleRoot == uint256("0x27f77273afc4e7cca700b8564eed9a7cc7ee38e81189a8d57a98bc42f848d51e"));
|
|
assert(block.GetHash() == (!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet));
|
|
|
|
// Start new block file
|
|
unsigned int nFile;
|
|
unsigned int nBlockPos;
|
|
if (!block.WriteToDisk(nFile, nBlockPos))
|
|
return error("LoadBlockIndex() : writing genesis block to disk failed");
|
|
if (!block.AddToBlockIndex(nFile, nBlockPos, 0))
|
|
return error("LoadBlockIndex() : genesis block not accepted");
|
|
|
|
// triangles: initialize synchronized checkpoint
|
|
if (!Checkpoints::WriteSyncCheckpoint((!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet)))
|
|
return error("LoadBlockIndex() : failed to init sync checkpoint");
|
|
}
|
|
|
|
string strPubKey = "";
|
|
|
|
// if checkpoint master key changed must reset sync-checkpoint
|
|
if (!txdb.ReadCheckpointPubKey(strPubKey) || strPubKey != CSyncCheckpoint::strMasterPubKey)
|
|
{
|
|
// write checkpoint master key to db
|
|
txdb.TxnBegin();
|
|
if (!txdb.WriteCheckpointPubKey(CSyncCheckpoint::strMasterPubKey))
|
|
return error("LoadBlockIndex() : failed to write new checkpoint master key to db");
|
|
if (!txdb.TxnCommit())
|
|
return error("LoadBlockIndex() : failed to commit new checkpoint master key to db");
|
|
if ((!fTestNet) && !Checkpoints::ResetSyncCheckpoint())
|
|
{
|
|
// For snapshot-sourced chains, the small initial block index may
|
|
// not include any of the known sync checkpoints yet (snapshot only
|
|
// includes ~1166 headers near tip). The sync checkpoint will be
|
|
// set when the node syncs past a known checkpoint height.
|
|
if (fLoadedFromSnapshot) {
|
|
printf("LoadBlockIndex(): sync-checkpoint reset deferred (snapshot-sourced, no checkpoints in small index yet)\n");
|
|
} else {
|
|
return error("LoadBlockIndex() : failed to reset sync-checkpoint");
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
void PrintBlockTree()
|
|
{
|
|
// pre-compute tree structure
|
|
map<CBlockIndex*, vector<CBlockIndex*> > mapNext;
|
|
for (const auto& [hash, pindex] : mapBlockIndex)
|
|
{
|
|
mapNext[pindex->pprev].push_back(pindex);
|
|
// test
|
|
//while (rand() % 3 == 0)
|
|
// mapNext[pindex->pprev].push_back(pindex);
|
|
}
|
|
|
|
vector<pair<int, CBlockIndex*> > vStack;
|
|
vStack.push_back(make_pair(0, pindexGenesisBlock));
|
|
|
|
int nPrevCol = 0;
|
|
while (!vStack.empty())
|
|
{
|
|
int nCol = vStack.back().first;
|
|
CBlockIndex* pindex = vStack.back().second;
|
|
vStack.pop_back();
|
|
|
|
// print split or gap
|
|
if (nCol > nPrevCol)
|
|
{
|
|
for (int i = 0; i < nCol-1; i++)
|
|
printf("| ");
|
|
printf("|\\\n");
|
|
}
|
|
else if (nCol < nPrevCol)
|
|
{
|
|
for (int i = 0; i < nCol; i++)
|
|
printf("| ");
|
|
printf("|\n");
|
|
}
|
|
nPrevCol = nCol;
|
|
|
|
// print columns
|
|
for (int i = 0; i < nCol; i++)
|
|
printf("| ");
|
|
|
|
// print item
|
|
CBlock block;
|
|
block.ReadFromDisk(pindex);
|
|
printf("%d (%u,%u) %s %08x %s mint %7s tx %" PRIszu "",
|
|
pindex->nHeight,
|
|
pindex->nFile,
|
|
pindex->nBlockPos,
|
|
block.GetHash().ToString().c_str(),
|
|
block.nBits,
|
|
DateTimeStrFormat("%x %H:%M:%S", block.GetBlockTime()).c_str(),
|
|
FormatMoney(pindex->nMint).c_str(),
|
|
block.vtx.size());
|
|
|
|
PrintWallets(block);
|
|
|
|
// put the main time-chain first
|
|
vector<CBlockIndex*>& vNext = mapNext[pindex];
|
|
for (unsigned int i = 0; i < vNext.size(); i++)
|
|
{
|
|
if (vNext[i]->pnext)
|
|
{
|
|
swap(vNext[0], vNext[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// iterate children
|
|
for (unsigned int i = 0; i < vNext.size(); i++)
|
|
vStack.push_back(make_pair(nCol+i, vNext[i]));
|
|
}
|
|
}
|
|
|
|
bool LoadExternalBlockFile(FILE* fileIn)
|
|
{
|
|
int64_t nStart = GetTimeMillis();
|
|
|
|
// Get file size for progress reporting
|
|
int64_t nFileSize = 0;
|
|
fseek(fileIn, 0, SEEK_END);
|
|
nFileSize = ftell(fileIn);
|
|
fseek(fileIn, 0, SEEK_SET);
|
|
|
|
int nLoaded = 0;
|
|
int64_t nLastProgressReport = 0;
|
|
{
|
|
LOCK(cs_main);
|
|
try {
|
|
CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION);
|
|
unsigned int nPos = 0;
|
|
while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown)
|
|
{
|
|
unsigned char pchData[65536];
|
|
do {
|
|
fseek(blkdat, nPos, SEEK_SET);
|
|
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
|
|
if (nRead <= 8)
|
|
{
|
|
nPos = (unsigned int)-1;
|
|
break;
|
|
}
|
|
void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart));
|
|
if (nFind)
|
|
{
|
|
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0)
|
|
{
|
|
nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart);
|
|
break;
|
|
}
|
|
nPos += ((unsigned char*)nFind - pchData) + 1;
|
|
}
|
|
else
|
|
nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1;
|
|
} while(!fRequestShutdown);
|
|
if (nPos == (unsigned int)-1)
|
|
break;
|
|
fseek(blkdat, nPos, SEEK_SET);
|
|
unsigned int nSize;
|
|
blkdat >> nSize;
|
|
if (nSize > 0 && nSize <= MAX_BLOCK_SIZE)
|
|
{
|
|
CBlock block;
|
|
blkdat >> block;
|
|
|
|
// Quick check: skip blocks we already have in the index.
|
|
// This avoids full ProcessBlock overhead during resumed imports.
|
|
uint256 hash = block.GetHash();
|
|
if (mapBlockIndex.count(hash))
|
|
{
|
|
// Already indexed - skip silently
|
|
}
|
|
else if (ProcessBlock(nullptr,&block))
|
|
nLoaded++;
|
|
nPos += 4 + nSize;
|
|
}
|
|
|
|
// Report progress every 1000 blocks
|
|
if (nLoaded - nLastProgressReport >= 1000)
|
|
{
|
|
nLastProgressReport = nLoaded;
|
|
if (nFileSize > 0) {
|
|
int pct = (int)((int64_t)nPos * 100 / nFileSize);
|
|
printf("Importing blocks... %d blocks loaded (%d%%)\n", nLoaded, pct);
|
|
uiInterface.InitMessage(strprintf(_("Importing blocks... %d loaded (%d%%)"), nLoaded, pct));
|
|
} else {
|
|
printf("Importing blocks... %d blocks loaded\n", nLoaded);
|
|
uiInterface.InitMessage(strprintf(_("Importing blocks... %d loaded"), nLoaded));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (std::exception &e) {
|
|
printf("%s() : Deserialize or I/O error caught during load\n",
|
|
__PRETTY_FUNCTION__);
|
|
}
|
|
}
|
|
printf("Loaded %i blocks from external file in %" PRId64 "ms\n", nLoaded, GetTimeMillis() - nStart);
|
|
return nLoaded > 0;
|
|
}
|
|
|
|
|
|
bool FastImportBlockFile()
|
|
{
|
|
// Explicit recovery importer: read the single raw block file used by this
|
|
// storage format and reconstruct all derived chain state without writing
|
|
// to blk0001.dat. The caller gates this behind -reindex.
|
|
|
|
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
|
if (!fs::exists(blkPath))
|
|
return false;
|
|
|
|
// LoadBlockIndex normally initializes these before opening the database.
|
|
// Reindex bypasses its genesis-creation path, so initialize the same
|
|
// network-specific framing and consensus parameters here.
|
|
if (fTestNet)
|
|
{
|
|
pchMessageStart[0] = 0x6f;
|
|
pchMessageStart[1] = 0x3e;
|
|
pchMessageStart[2] = 0x04;
|
|
pchMessageStart[3] = 0x13;
|
|
bnProofOfStakeLimit = bnProofOfStakeLimitTestNet;
|
|
bnProofOfWorkLimit = bnProofOfWorkLimitTestNet;
|
|
nStakeMinAge = 10 * 60;
|
|
nStakeMaxAge = 30 * 60;
|
|
nModifierInterval = 60;
|
|
nCoinbaseMaturity = 10;
|
|
nTargetSpacing = 60;
|
|
}
|
|
|
|
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
|
|
int64_t nStart = GetTimeMillis();
|
|
|
|
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
|
|
if (!fileIn)
|
|
return false;
|
|
std::unique_ptr<FILE, int(*)(FILE*)> fileGuard(fileIn, &fclose);
|
|
|
|
// Get file size for progress
|
|
if (fseek(fileIn, 0, SEEK_END) != 0)
|
|
return error("FastImportBlockFile: cannot seek to end of blk0001.dat");
|
|
int64_t nFileSize = ftell(fileIn);
|
|
if (nFileSize <= 0 || nFileSize > (int64_t)std::numeric_limits<unsigned int>::max() ||
|
|
fseek(fileIn, 0, SEEK_SET) != 0)
|
|
return error("FastImportBlockFile: blk0001.dat size is invalid or exceeds the 32-bit disk-position format");
|
|
|
|
int nLoaded = 0;
|
|
int nRootBlocks = 0;
|
|
int64_t nLastRecordEnd = 0;
|
|
const uint256 expectedGenesis = fTestNet ? hashGenesisBlockTestNet : hashGenesisBlockOfficial;
|
|
|
|
{
|
|
LOCK(cs_main);
|
|
|
|
auto txdb_holder = MakeChainDB("cr+"); CTxDBBase& txdb = *txdb_holder;
|
|
if (!txdb.TxnBegin())
|
|
return error("FastImportBlockFile: failed to begin database transaction");
|
|
|
|
try
|
|
{
|
|
// The entire import runs inside this try block. The catch below
|
|
// guarantees the in-flight transaction is explicitly aborted on
|
|
// any exception (allocation, database, validation, or otherwise)
|
|
// before propagating, so a partial commit cannot leak even if the
|
|
// inner error paths miss a TxnAbort. Each inner error path also
|
|
// aborts explicitly for clarity.
|
|
|
|
unsigned int nPos = 0;
|
|
while ((int64_t)nPos < nFileSize && !fRequestShutdown)
|
|
{
|
|
// Strict contiguous framing: every record must begin exactly at
|
|
// nPos with network magic + declared payload size. Do not scan
|
|
// forward through garbage; recovery must prove the whole file.
|
|
if (nFileSize - nPos < (int64_t)(sizeof(pchMessageStart) + sizeof(uint32_t)))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: truncated record header at file offset %u", nPos);
|
|
}
|
|
unsigned char recordMagic[sizeof(pchMessageStart)];
|
|
if (fseek(fileIn, nPos, SEEK_SET) != 0 ||
|
|
fread(recordMagic, 1, sizeof(recordMagic), fileIn) != sizeof(recordMagic) ||
|
|
memcmp(recordMagic, pchMessageStart, sizeof(recordMagic)) != 0)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: invalid record magic at file offset %u", nPos);
|
|
}
|
|
|
|
uint32_t nSize = 0;
|
|
if (fread(&nSize, sizeof(nSize), 1, fileIn) != 1)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: cannot read block size at file offset %u", nPos);
|
|
}
|
|
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: invalid block size %u at file offset %u", nSize, nPos);
|
|
}
|
|
const int64_t payloadPos = (int64_t)nPos + sizeof(pchMessageStart) + sizeof(nSize);
|
|
if (payloadPos + nSize > nFileSize)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: truncated block record at file offset %u", nPos);
|
|
}
|
|
|
|
std::vector<char> payload(nSize);
|
|
if (fread(payload.data(), 1, nSize, fileIn) != nSize)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: short payload read at file offset %u", nPos);
|
|
}
|
|
|
|
const unsigned int nBlockPos = (unsigned int)payloadPos;
|
|
CBlock block;
|
|
try
|
|
{
|
|
CDataStream record(payload.data(), payload.data() + payload.size(),
|
|
SER_DISK, CLIENT_VERSION);
|
|
record >> block;
|
|
if (!record.empty())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: block payload has %" PRIszu " trailing bytes at file offset %u",
|
|
record.size(), nPos);
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: malformed block payload at file offset %u: %s",
|
|
nPos, e.what());
|
|
}
|
|
catch (...)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: unknown deserialization failure at file offset %u",
|
|
nPos);
|
|
}
|
|
|
|
const unsigned int nRecordEnd = (unsigned int)(payloadPos + nSize);
|
|
|
|
// Reindex is optimized for trusted local history but must still
|
|
// apply every context-free block/transaction invariant before it
|
|
// can write derived state. Context-dependent chain validity is
|
|
// anchored below by exact genesis, parent continuity, cumulative
|
|
// trust selection, and all compiled hardened checkpoints.
|
|
//
|
|
// PoS block-signature verification follows the runtime rule:
|
|
// - blocks above the newest compiled checkpoint must be
|
|
// individually signed and chain-trust valid;
|
|
// - blocks at or below the newest compiled checkpoint are
|
|
// covered by the historical assume-valid fast path, which
|
|
// is the same rule the daemon uses at runtime. We must NOT
|
|
// apply the per-block signature check unconditionally,
|
|
// because that policy change was deliberately added in
|
|
// v6.x to prevent chain splits over the pre-checkpoint era.
|
|
if (!block.CheckBlock(true, true, false))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: block failed context-free validation at file offset %u",
|
|
nPos);
|
|
}
|
|
if (block.IsProofOfStake() && pindexBest->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
|
!block.CheckBlockSignature())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: post-checkpoint block signature failure at file offset %u",
|
|
nPos);
|
|
}
|
|
|
|
uint256 hash = block.GetHash();
|
|
if (block.hashPrevBlock == 0)
|
|
{
|
|
// The expected network genesis is the very first record in the
|
|
// file (offset 0). The runtime rule is "blocks whose parent is
|
|
// zero are only the genesis", and any other record with a zero
|
|
// parent would corrupt the active chain, so reject anything
|
|
// that hashes to the genesis hash anywhere other than offset 0.
|
|
++nRootBlocks;
|
|
if (hash == expectedGenesis)
|
|
{
|
|
if (nPos != 0 || nRootBlocks != 1)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: unexpected or duplicate genesis block %s at file offset %u",
|
|
hash.ToString().c_str(), nPos);
|
|
}
|
|
}
|
|
else if (nPos == 0)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: first record is not the expected genesis block %s",
|
|
hash.ToString().c_str());
|
|
}
|
|
else
|
|
{
|
|
// Stray root record (previous broken -reindex runs may have
|
|
// appended a fresh genesis record to blk0001.dat). Skip it:
|
|
// it has no parent, no chain trust, and would otherwise be
|
|
// a false duplicate of genesis. Advance strictly so the
|
|
// exact-file-consumed invariant still holds.
|
|
nPos = nRecordEnd;
|
|
nLastRecordEnd = nPos;
|
|
nLoaded++;
|
|
continue;
|
|
}
|
|
}
|
|
if (mapBlockIndex.count(hash))
|
|
{
|
|
nPos = nRecordEnd;
|
|
nLastRecordEnd = nPos;
|
|
continue; // already indexed
|
|
}
|
|
|
|
// Create CBlockIndex
|
|
CBlockIndex* pindexNew = new CBlockIndex(1, nBlockPos, block);
|
|
if (!pindexNew)
|
|
break;
|
|
|
|
// Link to previous block
|
|
auto miPrev = mapBlockIndex.find(block.hashPrevBlock);
|
|
if (miPrev != mapBlockIndex.end())
|
|
{
|
|
pindexNew->pprev = miPrev->second;
|
|
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
|
}
|
|
else if (block.hashPrevBlock != 0)
|
|
{
|
|
delete pindexNew;
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: parent %s missing before block %s",
|
|
block.hashPrevBlock.ToString().c_str(), hash.ToString().c_str());
|
|
}
|
|
|
|
if (!Checkpoints::CheckHardened(pindexNew->nHeight, hash))
|
|
{
|
|
const int badHeight = pindexNew->nHeight;
|
|
delete pindexNew;
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: hardened checkpoint mismatch at height %d",
|
|
badHeight);
|
|
}
|
|
if (pindexNew->nHeight > Checkpoints::GetLastCheckpointHeight() &&
|
|
!block.CheckBlockSignature())
|
|
{
|
|
const int badHeight = pindexNew->nHeight;
|
|
delete pindexNew;
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: post-checkpoint block signature failure at height %d",
|
|
badHeight);
|
|
}
|
|
|
|
// Chain trust
|
|
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
|
|
|
// Stake entropy bit
|
|
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
|
|
|
|
// Recompute the exact historical stake-modifier chain. Every
|
|
// block must participate: using placeholder zero modifiers for
|
|
// older blocks leaves mature wallet UTXOs unable to resolve the
|
|
// later modifier required by CheckStakeKernelHash(). Reindex is
|
|
// an explicit recovery operation, so correctness takes priority
|
|
// over the old shortcut's speed.
|
|
uint64_t nStakeModifier = 0;
|
|
bool fGeneratedStakeModifier = false;
|
|
if (!ComputeNextStakeModifier(pindexNew->pprev,
|
|
nStakeModifier,
|
|
fGeneratedStakeModifier))
|
|
{
|
|
delete pindexNew;
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to compute stake modifier for block %s",
|
|
hash.ToString().c_str());
|
|
}
|
|
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
|
|
|
// Insert into mapBlockIndex and set phashBlock BEFORE calling
|
|
// GetStakeModifierChecksum, which calls GetBlockHash() which
|
|
// dereferences phashBlock. Without this ordering, phashBlock is
|
|
// null and the checksum call segfaults.
|
|
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
|
|
pindexNew->phashBlock = &mi->first;
|
|
|
|
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
|
|
|
// PoS stake seen set
|
|
if (pindexNew->IsProofOfStake())
|
|
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
|
|
|
// pnext is rebuilt after best-chain selection. File order also
|
|
// contains side branches, so assigning it here would let the last
|
|
// imported child hijack stake-modifier forward walks.
|
|
|
|
// NOTE: tx-index, UTXO-set and money-supply application are
|
|
// DEFERRED to a second pass over the active (best-trust) chain
|
|
// only — see the pass after this loop. Applying them here, for
|
|
// every block read from the file (which permanently retains
|
|
// ORPHANED side-chain blocks), wrote those orphans' outputs into
|
|
// the UTXO set as phantom coins and over-counted nMoneySupply.
|
|
// That was the root cause of UTXO-set / supply inflation on every
|
|
// reindex. Here we only build the block index for all blocks so
|
|
// best-chain selection by trust still works.
|
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew)))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to write block index %s",
|
|
hash.ToString().c_str());
|
|
}
|
|
|
|
// Update best chain
|
|
if (pindexNew->nChainTrust > nBestChainTrust)
|
|
{
|
|
hashBestChain = hash;
|
|
pindexBest = pindexNew;
|
|
pblockindexFBBHLast = nullptr;
|
|
nBestHeight = pindexNew->nHeight;
|
|
nBestChainTrust = pindexNew->nChainTrust;
|
|
nTimeBestReceived = GetTime();
|
|
}
|
|
|
|
// Set genesis block
|
|
if (pindexGenesisBlock == nullptr && pindexNew->nHeight == 0)
|
|
pindexGenesisBlock = pindexNew;
|
|
|
|
nLoaded++;
|
|
nPos = nRecordEnd;
|
|
nLastRecordEnd = nPos;
|
|
|
|
// Batch commit every 200K blocks for LevelDB efficiency
|
|
if (nLoaded % 200000 == 0)
|
|
{
|
|
if (!txdb.WriteHashBestChain(hashBestChain))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: index batch WriteHashBestChain failed after %d blocks", nLoaded);
|
|
}
|
|
if (!txdb.TxnCommit())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: index batch TxnCommit failed after %d blocks", nLoaded);
|
|
}
|
|
if (!txdb.TxnBegin())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: index batch TxnBegin failed after %d blocks", nLoaded);
|
|
}
|
|
}
|
|
|
|
// Report progress every 5000 blocks to keep GUI responsive.
|
|
// AppInit2 runs on the GUI thread, so uiInterface.InitMessage
|
|
// triggers processEvents() which prevents the window from freezing.
|
|
if (nLoaded % 5000 == 0)
|
|
{
|
|
int pct = (nFileSize > 0) ? (int)((int64_t)nPos * 100 / nFileSize) : 0;
|
|
printf("FastImport: %d blocks indexed (%d%%)\n", nLoaded, pct);
|
|
uiInterface.InitMessage(strprintf(_("Importing blocks... %d indexed (%d%%)"), nLoaded, pct));
|
|
}
|
|
}
|
|
|
|
if (fRequestShutdown)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: interrupted after %d blocks; reindex is incomplete", nLoaded);
|
|
}
|
|
if (nRootBlocks < 1 || nLastRecordEnd != nFileSize)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: block file was not consumed exactly (roots=%d end=%" PRId64 " size=%" PRId64 ")",
|
|
nRootBlocks, nLastRecordEnd, nFileSize);
|
|
}
|
|
if (!pindexBest || !pindexGenesisBlock ||
|
|
pindexGenesisBlock->GetBlockHash() != expectedGenesis)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: no complete active chain found");
|
|
}
|
|
|
|
const int requiredCheckpointHeight = Checkpoints::GetLastCheckpointHeight();
|
|
CBlockIndex* requiredCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
|
if (requiredCheckpointHeight < 0 || !requiredCheckpoint ||
|
|
requiredCheckpoint->nHeight != requiredCheckpointHeight)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: selected chain does not reach the newest compiled checkpoint at height %d",
|
|
requiredCheckpointHeight);
|
|
}
|
|
CBlockIndex* checkpointAncestor = pindexBest;
|
|
while (checkpointAncestor && checkpointAncestor->nHeight > requiredCheckpointHeight)
|
|
checkpointAncestor = checkpointAncestor->pprev;
|
|
if (checkpointAncestor != requiredCheckpoint)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: newest compiled checkpoint is not on selected active chain");
|
|
}
|
|
|
|
// ---- Pass 2: apply tx-index, UTXO set and money supply along the
|
|
// ACTIVE (best-trust) chain ONLY. The file-order pass above indexed
|
|
// every block including orphaned side-chain blocks; replaying only
|
|
// the main chain here keeps the UTXO set and money supply exactly in
|
|
// consensus and prevents orphan outputs becoming phantom coins. ----
|
|
if (pindexBest)
|
|
{
|
|
std::vector<CBlockIndex*> vMain;
|
|
for (CBlockIndex* p = pindexBest; p; p = p->pprev)
|
|
vMain.push_back(p);
|
|
std::reverse(vMain.begin(), vMain.end());
|
|
|
|
// File order includes side branches. Build pnext exclusively from
|
|
// the selected best-trust chain so kernel-modifier forward walks
|
|
// cannot follow whichever side-chain child appeared last.
|
|
for (const auto& item : mapBlockIndex)
|
|
item.second->pnext = nullptr;
|
|
for (size_t i = 1; i < vMain.size(); ++i)
|
|
vMain[i - 1]->pnext = vMain[i];
|
|
|
|
printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size());
|
|
uiInterface.InitMessage(_("Building UTXO set (main chain)..."));
|
|
|
|
int64_t nRunningSupply = 0;
|
|
int nApplied = 0;
|
|
for (CBlockIndex* pindex : vMain)
|
|
{
|
|
if (fRequestShutdown)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: interrupted during active-chain replay at height %d",
|
|
pindex->nHeight);
|
|
}
|
|
|
|
// Genesis (height 0) is a hardcoded special block that is not
|
|
// re-read from disk this way; it contributes nothing to supply
|
|
// and the genesis-walk audit skips it identically. Carry the
|
|
// running supply (0) forward and move on.
|
|
if (pindex->nHeight == 0)
|
|
{
|
|
pindex->nMint = 0;
|
|
pindex->nMoneySupply = nRunningSupply; // still 0 here
|
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to write genesis index");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
CBlock blockMain;
|
|
if (!blockMain.ReadFromDisk(pindex))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight);
|
|
}
|
|
|
|
int64_t nBlockValueIn = 0;
|
|
int64_t nBlockValueOut = 0;
|
|
unsigned int nTxPos2 = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
|
|
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(blockMain.vtx.size());
|
|
for (const CTransaction& tx : blockMain.vtx)
|
|
{
|
|
uint256 hashTx = tx.GetHash();
|
|
CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2);
|
|
if (!txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size())))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to write txindex %s",
|
|
hashTx.ToString().c_str());
|
|
}
|
|
nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
|
|
|
nBlockValueOut += tx.GetValueOut();
|
|
if (!tx.IsCoinBase())
|
|
{
|
|
for (const CTxIn& txin : tx.vin)
|
|
{
|
|
CUtxoEntry uprev;
|
|
if (!txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: missing spent UTXO %s:%u at height %d",
|
|
txin.prevout.hash.ToString().c_str(), txin.prevout.n,
|
|
pindex->nHeight);
|
|
}
|
|
nBlockValueIn += uprev.nValue;
|
|
if (!txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to erase spent UTXO %s:%u",
|
|
txin.prevout.hash.ToString().c_str(), txin.prevout.n);
|
|
}
|
|
}
|
|
}
|
|
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
|
{
|
|
if (tx.vout[k].IsEmpty())
|
|
continue;
|
|
CUtxoEntry utxo;
|
|
utxo.nValue = tx.vout[k].nValue;
|
|
utxo.nHeight = pindex->nHeight;
|
|
utxo.scriptPubKey = tx.vout[k].scriptPubKey;
|
|
utxo.fCoinBase = tx.IsCoinBase();
|
|
utxo.fCoinStake = tx.IsCoinStake();
|
|
utxo.nTxTime = tx.nTime;
|
|
if (!txdb.WriteUtxo(hashTx, k, utxo))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to write UTXO %s:%u",
|
|
hashTx.ToString().c_str(), k);
|
|
}
|
|
}
|
|
}
|
|
|
|
pindex->nMint = nBlockValueOut - nBlockValueIn;
|
|
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
|
pindex->nMoneySupply = nRunningSupply;
|
|
if (!txdb.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to update active block index at height %d",
|
|
pindex->nHeight);
|
|
}
|
|
|
|
if (++nApplied % 200000 == 0)
|
|
{
|
|
if (!txdb.TxnCommit())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: active-chain batch TxnCommit failed at height %d",
|
|
pindex->nHeight);
|
|
}
|
|
if (!txdb.TxnBegin())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: active-chain batch TxnBegin failed at height %d",
|
|
pindex->nHeight);
|
|
}
|
|
}
|
|
if (nApplied % 5000 == 0)
|
|
{
|
|
int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size()));
|
|
printf("FastImport UTXO apply: %d/%d main-chain blocks (%d%%)\n", nApplied, (int)vMain.size(), pct2);
|
|
uiInterface.InitMessage(strprintf(_("Building UTXO set... %d%%"), pct2));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final commit
|
|
if (fRequestShutdown)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: interrupted before final commit");
|
|
}
|
|
if (!txdb.WriteHashBestChain(hashBestChain))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to persist best-chain hash");
|
|
}
|
|
|
|
// Write sync checkpoint
|
|
if (!Checkpoints::WriteSyncCheckpoint(hashBestChain))
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: failed to persist sync checkpoint");
|
|
}
|
|
|
|
if (!txdb.TxnCommit())
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: final database commit failed");
|
|
}
|
|
} // end try { ... FastImportBlockFile inner LOCK }
|
|
catch (const std::exception& e)
|
|
{
|
|
// Any exception escaping the import (allocation failure, database
|
|
// throw, unexpected validation throw) MUST NOT leak a partial
|
|
// commit. Abort the in-flight transaction before propagating.
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: uncaught exception during import: %s", e.what());
|
|
}
|
|
catch (...)
|
|
{
|
|
txdb.TxnAbort();
|
|
return error("FastImportBlockFile: unknown exception during import");
|
|
}
|
|
}
|
|
|
|
nTransactionsUpdated++;
|
|
printf("FastImportBlockFile: indexed %d blocks in %" PRId64 "ms\n", nLoaded, GetTimeMillis() - nStart);
|
|
return nLoaded > 0;
|
|
}
|
|
|
|
string GetWarnings(string strFor)
|
|
{
|
|
string strStatusBar;
|
|
string strRPC;
|
|
|
|
// triangles: if detected invalid checkpoint enter safe mode
|
|
if (Checkpoints::hashInvalidCheckpoint != 0)
|
|
strStatusBar = strRPC = _("WARNING: Invalid checkpoint found! Displayed transactions may not be correct! You may need to upgrade, or notify developers.");
|
|
|
|
if (strFor == "statusbar")
|
|
return strStatusBar;
|
|
else if (strFor == "rpc")
|
|
return strRPC;
|
|
assert(!"GetWarnings() : invalid parameter");
|
|
return "error";
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Messages
|
|
//
|
|
|
|
|
|
bool static AlreadyHave(CTxDBBase& txdb, const CInv& inv)
|
|
{
|
|
switch (inv.type)
|
|
{
|
|
case MSG_TX:
|
|
{
|
|
bool txInMap = false;
|
|
{
|
|
LOCK(mempool.cs);
|
|
txInMap = (mempool.exists(inv.hash));
|
|
}
|
|
return txInMap ||
|
|
mapOrphanTransactions.count(inv.hash) ||
|
|
txdb.ContainsTx(inv.hash);
|
|
}
|
|
|
|
case MSG_BLOCK:
|
|
case MSG_CMPCT_BLOCK:
|
|
return mapBlockIndex.count(inv.hash) ||
|
|
mapOrphanBlocks.count(inv.hash);
|
|
}
|
|
// Don't know what it is, just say we already got one
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
// The message start string is designed to be unlikely to occur in normal data.
|
|
// The characters are rarely used upper ASCII, not valid as UTF-8, and produce
|
|
// a large 4-byte int at any alignment.
|
|
unsigned char pchMessageStart[4] = { 0x70, 0x35, 0x22, 0x05 };
|
|
|
|
bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
|
{
|
|
RandAddSeedPerfmon();
|
|
if (fDebug)
|
|
printf("received: %s (%" PRIszu " bytes)\n", strCommand.c_str(), vRecv.size());
|
|
if (mapArgs.count("-dropmessagestest") && GetRand(atoi(mapArgs["-dropmessagestest"])) == 0)
|
|
{
|
|
printf("dropmessagestest DROPPING RECV MESSAGE\n");
|
|
return true;
|
|
}
|
|
|
|
if (strCommand == "version")
|
|
{
|
|
// Each connection can only send one version message
|
|
if (pfrom->nVersion != 0)
|
|
{
|
|
pfrom->Misbehaving(1);
|
|
return false;
|
|
}
|
|
|
|
int64_t nTime;
|
|
CAddress addrMe;
|
|
CAddress addrFrom;
|
|
uint64_t nNonce = 1;
|
|
//uint64_t verification_token = 0;
|
|
vRecv >> pfrom->nVersion >> pfrom->nServices >> nTime >> addrMe;
|
|
if (pfrom->nVersion < MIN_PROTO_VERSION)
|
|
{
|
|
// Since February 20, 2012, the protocol is initiated at version 209,
|
|
// and earlier versions are no longer supported
|
|
printf("partner %s using obsolete version %i; disconnecting\n", pfrom->addr.ToString().c_str(), pfrom->nVersion);
|
|
pfrom->fDisconnect = true;
|
|
return false;
|
|
}
|
|
|
|
if (pfrom->nVersion == 10300)
|
|
pfrom->nVersion = 300;
|
|
if (!vRecv.empty())
|
|
vRecv >> addrFrom >> nNonce;
|
|
if (!vRecv.empty()) {
|
|
vRecv >> pfrom->strSubVer;
|
|
//pfrom->cleanSubVer = SanitizeString(pfrom->strSubVer);
|
|
}
|
|
if (!vRecv.empty())
|
|
vRecv >> pfrom->nStartingHeight;
|
|
|
|
if (pfrom->fInbound && addrMe.IsRoutable())
|
|
{
|
|
pfrom->addrLocal = addrMe;
|
|
SeenLocal(addrMe);
|
|
}
|
|
|
|
// Disconnect if we connected to ourself
|
|
if (nNonce == nLocalHostNonce && nNonce > 1)
|
|
{
|
|
printf("connected to self at %s, disconnecting\n", pfrom->addr.ToString().c_str());
|
|
pfrom->fDisconnect = true;
|
|
return true;
|
|
}
|
|
|
|
// triangles: record my external IP reported by peer
|
|
if (addrFrom.IsRoutable() && addrMe.IsRoutable())
|
|
addrSeenByPeer = addrMe;
|
|
|
|
// Be shy and don't send version until we hear
|
|
if (pfrom->fInbound)
|
|
pfrom->PushVersion();
|
|
|
|
pfrom->fClient = !(pfrom->nServices & NODE_NETWORK);
|
|
|
|
if (GetBoolArg("-synctime", true))
|
|
AddTimeData(pfrom->addr, nTime);
|
|
|
|
// Change version
|
|
pfrom->PushMessage("verack");
|
|
pfrom->ssSend.SetVersion(min(pfrom->nVersion, PROTOCOL_VERSION));
|
|
|
|
if (!pfrom->fInbound)
|
|
{
|
|
// Advertise our address
|
|
if (!fNoListen && !IsInitialBlockDownload())
|
|
{
|
|
CAddress addr = GetLocalAddress(&pfrom->addr);
|
|
if (addr.IsRoutable())
|
|
pfrom->PushAddress(addr);
|
|
}
|
|
|
|
// Always request addresses — critical for Tor-only small networks
|
|
{
|
|
pfrom->PushMessage("getaddr");
|
|
pfrom->fGetAddr = true;
|
|
}
|
|
addrman.Good(pfrom->addr);
|
|
} else {
|
|
if (((CNetAddr)pfrom->addr) == (CNetAddr)addrFrom)
|
|
{
|
|
addrman.Add(addrFrom, addrFrom);
|
|
addrman.Good(addrFrom);
|
|
}
|
|
// Also request addresses from inbound peers (small network optimization)
|
|
if (!pfrom->fGetAddr) {
|
|
pfrom->PushMessage("getaddr");
|
|
pfrom->fGetAddr = true;
|
|
}
|
|
}
|
|
|
|
// Ask connected nodes for block updates.
|
|
// During IBD, request blocks from every valid peer to maximize download
|
|
// parallelism. Multiple peers sending overlapping inv ranges is harmless
|
|
// (AlreadyHave filters duplicates) but ensures we discover and download
|
|
// blocks from the fastest available source.
|
|
// NOTE: nStartingHeight from version messages is unverified. Peers can
|
|
// claim any height. During IBD we always ask all eligible peers rather
|
|
// than filtering on a claim that may be wrong (a stunted node could be
|
|
// reporting the full chain height while only serving the tail of its
|
|
// own fork). Use nBestKnownHeight (updated from actual block responses)
|
|
// for peer capability assessment instead.
|
|
static int nAskedForBlocks = 0;
|
|
bool fIBD = IsInitialBlockDownload();
|
|
// During IBD: ask every non-client peer unconditionally to maximise
|
|
// download sources. Post-IBD: use traditional height-check logic.
|
|
// During IBD we need to ask EVERY peer for blocks, including OneShot peers
|
|
// (those added via -addnode= and the hardcoded onion/i2p seed list). The previous
|
|
// `!pfrom->fOneShot` clause prevents getblocks/getheaders from being sent to these
|
|
// peers, which is exactly what fresh-from-genesis wallets need. Without this, a
|
|
// clean datadir syncs the first ~2000-4000 headers from one peer via the
|
|
// control-loop getheaders planner, then stalls because no version-handler
|
|
// getblocks was ever issued to fan out block requests.
|
|
bool fShouldAsk = !pfrom->fClient &&
|
|
(fIBD ||
|
|
pfrom->nStartingHeight > (nBestHeight - 144) ||
|
|
pfrom->nStartingHeight > nBestHeight) &&
|
|
(pfrom->nVersion < NOBLKS_VERSION_START ||
|
|
pfrom->nVersion >= NOBLKS_VERSION_END) &&
|
|
(fIBD || nAskedForBlocks < 1 || vNodes.size() <= 1 || pfrom->nStartingHeight > nBestHeight);
|
|
printf("IBD-DIAG: version handler: peer=%s height=%d ourHeight=%d fClient=%d fOneShot=%d shouldAsk=%d nAskedForBlocks=%d IBD=%d\n",
|
|
pfrom->addr.ToString().c_str(), pfrom->nStartingHeight, nBestHeight,
|
|
pfrom->fClient, pfrom->fOneShot, fShouldAsk, nAskedForBlocks, fIBD);
|
|
if (fShouldAsk)
|
|
{
|
|
nAskedForBlocks++;
|
|
pfrom->PushGetBlocks(pindexBest, uint256(0));
|
|
// During IBD, also send getheaders to scout the chain structure.
|
|
// Headers are ~80 bytes each (vs full blocks at ~1-2KB for PoS),
|
|
// so we learn about future blocks much faster. The headers handler
|
|
// will AskFor each unknown block, pre-populating the download queue.
|
|
if (fIBD)
|
|
g_syncManager.RequestRefill(pfrom, g_syncManager.GetBestHeader(), 0, "version bootstrap");
|
|
printf("IBD-DIAG: sent getblocks%s from height %d to peer %s\n",
|
|
fIBD ? "+getheaders" : "", nBestHeight, pfrom->addr.ToString().c_str());
|
|
}
|
|
|
|
// Sync checkpoint relay disabled (master key removed in V5 fork).
|
|
// Relaying stale checkpoints causes IBD nodes to request far-future blocks.
|
|
|
|
pfrom->fSuccessfullyConnected = true;
|
|
|
|
// Request header-based block announcements and compact block relay
|
|
pfrom->PushMessage("sendheaders");
|
|
pfrom->PushMessage("sendcmpct");
|
|
|
|
// If this is an onion peer and we have a pending resolve, request their wallet address
|
|
{
|
|
std::string peerAddr = pfrom->addr.ToStringIP();
|
|
if (peerAddr.find(".onion") != std::string::npos)
|
|
{
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
if (torMgr)
|
|
torMgr->RequestWalletAddress(pfrom);
|
|
}
|
|
}
|
|
|
|
printf("receive version message: version %d, blocks=%d, us=%s, them=%s, peer=%s\n", pfrom->nVersion, pfrom->nStartingHeight, addrMe.ToString().c_str(), addrFrom.ToString().c_str(), pfrom->addr.ToString().c_str());
|
|
|
|
cPeerBlockCounts.input(pfrom->nStartingHeight);
|
|
|
|
}
|
|
|
|
|
|
else if (pfrom->nVersion == 0)
|
|
{
|
|
// Must have a version message before anything else
|
|
pfrom->Misbehaving(1);
|
|
return false;
|
|
}
|
|
|
|
|
|
else if (strCommand == "verack")
|
|
{
|
|
pfrom->SetRecvVersion(min(pfrom->nVersion, PROTOCOL_VERSION));
|
|
}
|
|
|
|
|
|
else if (strCommand == "sendheaders")
|
|
{
|
|
// Peer prefers block announcements via headers instead of inv.
|
|
// When we have a new block, we'll send a "headers" message rather
|
|
// than waiting for the inv->getdata round-trip, saving ~2-4s on Tor.
|
|
pfrom->fPreferHeaders = true;
|
|
}
|
|
|
|
|
|
else if (strCommand == "sendcmpct")
|
|
{
|
|
// Peer supports BIP152 compact block relay.
|
|
// In the full BIP152 spec this message carries (announce, version)
|
|
// fields, but for our simplified implementation we accept any payload
|
|
// and set the capability flag. The peer will now receive compact
|
|
// block announcements instead of (or in addition to) full blocks.
|
|
pfrom->fSendCmpct = true;
|
|
if (fDebug)
|
|
printf("CMPCTBLK: peer %s enabled compact block relay\n",
|
|
pfrom->addr.ToString().c_str());
|
|
}
|
|
|
|
|
|
else if (strCommand == "addr")
|
|
{
|
|
vector<CAddress> vAddr;
|
|
vRecv >> vAddr;
|
|
|
|
// Don't want addr from older versions unless seeding
|
|
if (pfrom->nVersion < CADDR_TIME_VERSION && addrman.size() > 1000)
|
|
return true;
|
|
if (vAddr.size() > 1000)
|
|
{
|
|
pfrom->Misbehaving(20);
|
|
return error("message addr size() = %" PRIszu "", vAddr.size());
|
|
}
|
|
|
|
// Store the new addresses
|
|
vector<CAddress> vAddrOk;
|
|
int64_t nNow = GetAdjustedTime();
|
|
int64_t nSince = nNow - 10 * 60;
|
|
for (CAddress& addr : vAddr)
|
|
{
|
|
if (fShutdown)
|
|
return true;
|
|
if (addr.nTime <= 100000000 || addr.nTime > nNow + 10 * 60)
|
|
addr.nTime = nNow - 5 * 24 * 60 * 60;
|
|
pfrom->AddAddressKnown(addr);
|
|
bool fReachable = IsReachable(addr);
|
|
if (addr.nTime > nSince && !pfrom->fGetAddr && vAddr.size() <= 10 && addr.IsRoutable())
|
|
{
|
|
// Relay to a limited number of other nodes
|
|
{
|
|
LOCK(cs_vNodes);
|
|
// Use deterministic randomness to send to the same nodes for 24 hours
|
|
// at a time so the setAddrKnowns of the chosen nodes prevent repeats
|
|
static uint256 hashSalt;
|
|
if (hashSalt == 0)
|
|
hashSalt = GetRandHash();
|
|
uint64_t hashAddr = addr.GetHash();
|
|
uint256 hashRand = hashSalt ^ (hashAddr<<32) ^ ((GetTime()+hashAddr)/(24*60*60));
|
|
hashRand = Hash(BEGIN(hashRand), END(hashRand));
|
|
multimap<uint256, CNode*> mapMix;
|
|
for (CNode* pnode : vNodes)
|
|
{
|
|
if (pnode->nVersion < CADDR_TIME_VERSION)
|
|
continue;
|
|
unsigned int nPointer;
|
|
memcpy(&nPointer, &pnode, sizeof(nPointer));
|
|
uint256 hashKey = hashRand ^ nPointer;
|
|
hashKey = Hash(BEGIN(hashKey), END(hashKey));
|
|
mapMix.insert(make_pair(hashKey, pnode));
|
|
}
|
|
// Small network: relay to more peers so addresses propagate quickly
|
|
int nRelayNodes = fReachable ? (int)mapMix.size() : 1;
|
|
for (auto mi = mapMix.begin(); mi != mapMix.end() && nRelayNodes-- > 0; ++mi)
|
|
mi->second->PushAddress(addr);
|
|
}
|
|
}
|
|
// Do not store addresses outside our network
|
|
if (fReachable)
|
|
vAddrOk.push_back(addr);
|
|
}
|
|
addrman.Add(vAddrOk, pfrom->addr, 2 * 60 * 60);
|
|
if (vAddr.size() < 1000)
|
|
pfrom->fGetAddr = false;
|
|
if (pfrom->fOneShot)
|
|
pfrom->fDisconnect = true;
|
|
}
|
|
|
|
else if (strCommand == "inv")
|
|
{
|
|
vector<CInv> vInv;
|
|
vRecv >> vInv;
|
|
if (vInv.size() > MAX_INV_SZ)
|
|
{
|
|
pfrom->Misbehaving(20);
|
|
return error("message inv size() = %" PRIszu "", vInv.size());
|
|
}
|
|
|
|
// find last block in inv vector
|
|
unsigned int nLastBlock = (unsigned int)(-1);
|
|
int nBlockInv = 0, nTxInv = 0;
|
|
for (unsigned int nInv = 0; nInv < vInv.size(); nInv++) {
|
|
if (vInv[nInv].type == MSG_BLOCK) nBlockInv++;
|
|
else nTxInv++;
|
|
if (vInv[vInv.size() - 1 - nInv].type == MSG_BLOCK && nLastBlock == (unsigned int)(-1)) {
|
|
nLastBlock = vInv.size() - 1 - nInv;
|
|
}
|
|
}
|
|
printf("IBD-DIAG: inv received: %d blocks, %d tx from %s (our height=%d)\n",
|
|
nBlockInv, nTxInv, pfrom->addr.ToString().c_str(), nBestHeight);
|
|
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
int nNew = 0, nAlready = 0, nAboveBest = 0;
|
|
int nFirstInvHeight = -1, nLastInvHeight = -1;
|
|
for (unsigned int nInv = 0; nInv < vInv.size(); nInv++)
|
|
{
|
|
const CInv &inv = vInv[nInv];
|
|
|
|
if (fShutdown)
|
|
return true;
|
|
pfrom->AddInventoryKnown(inv);
|
|
|
|
bool fAlreadyHave = AlreadyHave(txdb, inv);
|
|
if (inv.type == MSG_BLOCK) {
|
|
if (fAlreadyHave) {
|
|
nAlready++;
|
|
if (auto mi = mapBlockIndex.find(inv.hash); mi != mapBlockIndex.end()) {
|
|
int h = mi->second->nHeight;
|
|
if (nFirstInvHeight == -1) nFirstInvHeight = h;
|
|
nLastInvHeight = h;
|
|
if (h > pfrom->nBestKnownHeight)
|
|
pfrom->nBestKnownHeight = h;
|
|
if (h > nBestHeight) nAboveBest++;
|
|
}
|
|
} else {
|
|
nNew++;
|
|
}
|
|
}
|
|
|
|
if (!fAlreadyHave)
|
|
pfrom->AskFor(inv);
|
|
else if (inv.type == MSG_BLOCK && mapOrphanBlocks.count(inv.hash)) {
|
|
pfrom->PushGetBlocks(pindexBest, GetOrphanRoot(mapOrphanBlocks[inv.hash].get()));
|
|
} else if (nInv == nLastBlock) {
|
|
// Continuation: walk forward from the last inv block.
|
|
// Don't jump to pindexBest — its CBlockLocator exponential
|
|
// spacing can map back to the same old match point, looping.
|
|
// Walking from the last inv block progresses linearly through
|
|
// the "already have" zone until we reach new blocks.
|
|
int nInvH = mapBlockIndex[inv.hash]->nHeight;
|
|
if (nInvH > nHighestInvWalk) {
|
|
nHighestInvWalk = nInvH;
|
|
hashHighestInvWalk = inv.hash;
|
|
}
|
|
pfrom->pindexLastGetBlocksBegin = nullptr; // reset dedup
|
|
pfrom->PushGetBlocks(mapBlockIndex[inv.hash], uint256(0));
|
|
printf("SYNC-DIAG: inv walk-forward from %d (best=%d, walk=%d)\n",
|
|
nInvH, nBestHeight, nHighestInvWalk);
|
|
}
|
|
|
|
Inventory(inv.hash);
|
|
}
|
|
if (nBlockInv > 0) {
|
|
printf("SYNC-DIAG: inv result: %d new, %d already have (%d above best=%d), range=%d..%d\n",
|
|
nNew, nAlready, nAboveBest, nBestHeight, nFirstInvHeight, nLastInvHeight);
|
|
if (nNew > 0 && nAlready > 0)
|
|
printf("SYNC-DIAG: *** FORK POINT CROSSED *** - downloading %d new blocks from canonical chain\n", nNew);
|
|
}
|
|
}
|
|
|
|
|
|
else if (strCommand == "getdata")
|
|
{
|
|
vector<CInv> vInv;
|
|
vRecv >> vInv;
|
|
if (vInv.size() > MAX_INV_SZ)
|
|
{
|
|
pfrom->Misbehaving(20);
|
|
return error("message getdata size() = %" PRIszu "", vInv.size());
|
|
}
|
|
|
|
if (fDebugNet || (vInv.size() != 1))
|
|
printf("received getdata (%" PRIszu " invsz)\n", vInv.size());
|
|
|
|
for (const CInv& inv : vInv)
|
|
{
|
|
if (fShutdown)
|
|
return true;
|
|
if (fDebugNet || (vInv.size() == 1))
|
|
printf("received getdata for: %s\n", inv.ToString().c_str());
|
|
|
|
if (inv.type == MSG_BLOCK || inv.type == MSG_CMPCT_BLOCK)
|
|
{
|
|
// Send block from disk
|
|
auto mi = mapBlockIndex.find(inv.hash);
|
|
if (mi != mapBlockIndex.end())
|
|
{
|
|
CBlock block;
|
|
block.ReadFromDisk(mi->second);
|
|
|
|
// BIP152: if the peer has negotiated compact block relay
|
|
// (fSendCmpct) and explicitly requested via MSG_CMPCT_BLOCK,
|
|
// respond with a compact block instead of a full block.
|
|
// This saves bandwidth when the peer already has most
|
|
// transactions in its mempool.
|
|
if (inv.type == MSG_CMPCT_BLOCK && pfrom->fSendCmpct)
|
|
{
|
|
SendCompactBlock(pfrom, block);
|
|
}
|
|
else
|
|
{
|
|
pfrom->PushMessage("block", block);
|
|
}
|
|
|
|
// Trigger them to send a getblocks request for the next batch of inventory
|
|
if (inv.hash == pfrom->hashContinue)
|
|
{
|
|
// Send the best block hash to trigger the next getblocks.
|
|
// Original code sent the last PoW block, but since PoW ended
|
|
// at block 9000, that always sent an ancient block causing
|
|
// thousands of redundant round-trips through known blocks.
|
|
vector<CInv> vInv;
|
|
vInv.push_back(CInv(MSG_BLOCK, hashBestChain));
|
|
pfrom->PushMessage("inv", vInv);
|
|
pfrom->hashContinue = 0;
|
|
}
|
|
}
|
|
}
|
|
else if (inv.IsKnownType())
|
|
{
|
|
// Send stream from relay memory
|
|
bool pushed = false;
|
|
{
|
|
LOCK(cs_mapRelay);
|
|
if (auto mi = mapRelay.find(inv); mi != mapRelay.end()) {
|
|
pfrom->PushMessage(inv.GetCommand(), mi->second);
|
|
pushed = true;
|
|
}
|
|
}
|
|
if (!pushed && inv.type == MSG_TX) {
|
|
LOCK(mempool.cs);
|
|
if (mempool.exists(inv.hash)) {
|
|
CTransaction tx = mempool.lookup(inv.hash);
|
|
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
|
|
ss.reserve(1000);
|
|
ss << tx;
|
|
pfrom->PushMessage("tx", ss);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track requests for our stuff
|
|
Inventory(inv.hash);
|
|
}
|
|
}
|
|
|
|
|
|
else if (strCommand == "getblocks")
|
|
{
|
|
CBlockLocator locator;
|
|
uint256 hashStop;
|
|
vRecv >> locator >> hashStop;
|
|
|
|
// Find the last block the caller has in the main chain
|
|
CBlockIndex* pindex = locator.GetBlockIndex();
|
|
|
|
// Detect incompatible fork: peer sent a locator with entries but
|
|
// GetBlockIndex() fell through to genesis (no locator hash matched
|
|
// our main chain). If the peer's tip isn't our genesis,
|
|
// they're on a completely different fork.
|
|
//
|
|
// triangles fix: instead of banning or disconnecting, always respond
|
|
// with our main chain blocks so a fork node can learn the canonical
|
|
// chain and reorganize. The fork node's client will automatically
|
|
// reorg when it receives blocks that form a longer or higher-work chain.
|
|
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
|
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
|
{
|
|
pfrom->nIncompatibleGetblocks++;
|
|
// triangles: after many failed attempts, reset — the peer may now be
|
|
// on the correct chain and we don't want to ban a node that's just
|
|
// learning about the main chain from us.
|
|
if (pfrom->nIncompatibleGetblocks > 10)
|
|
pfrom->nIncompatibleGetblocks = 0;
|
|
// triangles: NO return/ban here — fall through and serve main chain
|
|
// blocks so the forking peer can reorg to our chain.
|
|
printf("WARNING: peer %s getblocks locator has no common blocks — serving main chain from genesis (counter=%d, will reset after 10)\\n",
|
|
pfrom->addr.ToString().c_str(), pfrom->nIncompatibleGetblocks);
|
|
pindex = pindexGenesisBlock;
|
|
}
|
|
else if (pindex && pindex != pindexGenesisBlock)
|
|
{
|
|
// Peer matched a non-genesis block — they share our chain
|
|
pfrom->nIncompatibleGetblocks = 0;
|
|
}
|
|
|
|
// Send the rest of the chain
|
|
if (pindex)
|
|
pindex = pindex->pnext;
|
|
// Send larger batches when the requester is far behind (syncing).
|
|
// The original check used our own IBD state, but we're the seed node
|
|
// (fully synced), so it always returned 500. Check how far behind
|
|
// the requester is instead.
|
|
int nLimit = (pindex && pindexBest && pindexBest->nHeight - pindex->nHeight > 1000) ? 10000 : 500;
|
|
printf("IBD-DIAG: getblocks request from peer %s: start=%d stop=%s limit=%d\n",
|
|
pfrom->addr.ToString().c_str(), (pindex ? pindex->nHeight : -1),
|
|
hashStop.ToString().substr(0,20).c_str(), nLimit);
|
|
for (; pindex; pindex = pindex->pnext)
|
|
{
|
|
if (pindex->GetBlockHash() == hashStop)
|
|
{
|
|
printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,20).c_str());
|
|
// triangles: tell downloading node about the latest block if it's
|
|
// without risk being rejected due to stake connection check
|
|
if (hashStop != hashBestChain && pindex->GetBlockTime() + nStakeMinAge > pindexBest->GetBlockTime())
|
|
pfrom->PushInventory(CInv(MSG_BLOCK, hashBestChain));
|
|
break;
|
|
}
|
|
pfrom->PushInventory(CInv(MSG_BLOCK, pindex->GetBlockHash()));
|
|
if (--nLimit <= 0)
|
|
{
|
|
// When this block is requested, we'll send an inv that'll make them
|
|
// getblocks the next batch of inventory.
|
|
printf(" getblocks stopping at limit %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,20).c_str());
|
|
pfrom->hashContinue = pindex->GetBlockHash();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (strCommand == "getheaders")
|
|
{
|
|
CBlockLocator locator;
|
|
uint256 hashStop;
|
|
vRecv >> locator >> hashStop;
|
|
|
|
CBlockIndex* pindex = nullptr;
|
|
if (locator.IsNull())
|
|
{
|
|
// If locator is null, return the hashStop block
|
|
auto mi = mapBlockIndex.find(hashStop);
|
|
if (mi == mapBlockIndex.end())
|
|
return true;
|
|
pindex = mi->second;
|
|
}
|
|
else
|
|
{
|
|
// Find the last block the caller has in the main chain
|
|
pindex = locator.GetBlockIndex();
|
|
|
|
// triangles fix: handle broken pnext chain.
|
|
// GetBlockIndex() returns pindexGenesisBlock when no locator
|
|
// hash matches our main chain (peer is on a different fork or
|
|
// a stale local state). pindexGenesisBlock->pnext is always
|
|
// null, which would cause the for-loop below to send ZERO
|
|
// headers, leaving the peer stuck (logged as "getheaders -1").
|
|
//
|
|
// Mirror the getblocks handler: if the locator matches nothing
|
|
// on our main chain, serve our headers from genesis so the peer
|
|
// can discover the canonical chain. Then fall back to a tip-
|
|
// backwards walk if pnext is null for any other reason (this
|
|
// happens when LoadBlockIndex() didn't fully heal pnext links,
|
|
// or the chain was bootstrapped from a snapshot).
|
|
//
|
|
// fork-peer getheaders recovery (fix/consensus-convergence).
|
|
//
|
|
// A forked peer calls getheaders with a locator containing the
|
|
// highest blocks it knows. If none of those hashes are in our
|
|
// main chain, locator.GetBlockIndex() returns pindexGenesisBlock
|
|
// and the for-loop below would send zero headers (the peer
|
|
// already has genesis), leaving the forked peer stuck.
|
|
//
|
|
// Recovery rule:
|
|
// - If the locator contains pindexLastHardenedCheckpoint,
|
|
// serve headers starting after the checkpoint — the peer
|
|
// already has the checkpoint and needs canonical history
|
|
// forward.
|
|
// - Otherwise, serve from the last common ancestor (if any)
|
|
// of the locator against our main chain, falling back to
|
|
// pindexGenesisBlock so the peer can walk forward from
|
|
// scratch.
|
|
//
|
|
// We never re-anchor at pindexLastHardenedCheckpoint without
|
|
// confirming the peer already knows it; otherwise we'd hand
|
|
// them a header whose parent they don't have, which is the
|
|
// inverse of the recovery path we want.
|
|
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
|
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
|
{
|
|
bool fServed = false;
|
|
if (pindexLastHardenedCheckpoint)
|
|
{
|
|
if (locator.Has(pindexLastHardenedCheckpoint->GetBlockHash()))
|
|
{
|
|
printf("getheaders: peer locator contains hardened checkpoint %d — serving canonical headers from there\n",
|
|
pindexLastHardenedCheckpoint->nHeight);
|
|
pindex = pindexLastHardenedCheckpoint;
|
|
fServed = true;
|
|
}
|
|
else
|
|
{
|
|
printf("getheaders: peer locator lacks hardened checkpoint %d — falling back to last common ancestor\n",
|
|
pindexLastHardenedCheckpoint->nHeight);
|
|
}
|
|
}
|
|
if (!fServed)
|
|
{
|
|
// Last-common-ancestor walk via the public locator API. We can't
|
|
// iterate locator.vHave from outside the class (it's
|
|
// protected); CBlockLocator::FindCommonAncestorInMainChain
|
|
// does the walk for us and returns the deepest block
|
|
// we have on the main chain that the peer also knows.
|
|
// Falling back to genesis when no overlap exists
|
|
// ensures the peer gets a recoverable header chain.
|
|
CBlockIndex* pCommon = locator.FindCommonAncestorInMainChain();
|
|
if (pCommon)
|
|
{
|
|
// Found a block in our main chain that the peer also has.
|
|
// Serve headers starting from it. This handles BOTH cases:
|
|
// (a) peer is on our canonical chain past us (pCommon == our tip
|
|
// OR pCommon == pindexLastHardenedCheckpoint if peer tip is
|
|
// past our last checkpoint) — serve from pCommon so they get
|
|
// the headers they need without re-walking from genesis.
|
|
// (b) peer is on a divergent fork but shares our checkpoint
|
|
// hash in their locator — still serve from the checkpoint
|
|
// because they will validate against our chain. If the peer
|
|
// has actually reorged, they will disconnect from us anyway.
|
|
printf("getheaders: serving canonical headers from last common ancestor %d (peer may be on a fork)\n",
|
|
pCommon->nHeight);
|
|
pindex = pCommon;
|
|
}
|
|
else
|
|
{
|
|
// No overlap at all — peer is on a completely different chain.
|
|
// Serve from genesis so they can re-walk and discover our canonical.
|
|
printf("getheaders: peer locator has no common blocks — serving headers from genesis (peer on a long fork)\n");
|
|
pindex = pindexGenesisBlock;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pindex)
|
|
{
|
|
if (pindex->pnext)
|
|
{
|
|
pindex = pindex->pnext;
|
|
}
|
|
else
|
|
{
|
|
// pnext is null — fall back to walking from pindexBest
|
|
// backwards to find the block immediately after pindex
|
|
CBlockIndex* pWalk = pindexBest;
|
|
while (pWalk && pWalk->pprev != pindex)
|
|
pWalk = pWalk->pprev;
|
|
pindex = pWalk; // null if pindex is already the tip
|
|
}
|
|
}
|
|
}
|
|
|
|
vector<CBlock> vHeaders;
|
|
int nLimit = 2000;
|
|
printf("getheaders %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,20).c_str());
|
|
for (; pindex; pindex = pindex->pnext)
|
|
{
|
|
vHeaders.push_back(pindex->GetBlockHeader());
|
|
if (--nLimit <= 0 || pindex->GetBlockHash() == hashStop)
|
|
break;
|
|
}
|
|
pfrom->PushMessage("headers", vHeaders);
|
|
}
|
|
|
|
else if (strCommand == "headers")
|
|
{
|
|
vector<CBlock> vHeaders;
|
|
vRecv >> vHeaders;
|
|
if (!g_syncManager.ProcessHeaders(pfrom, vHeaders))
|
|
return false;
|
|
}
|
|
|
|
|
|
else if (strCommand == "tx")
|
|
{
|
|
vector<uint256> vWorkQueue;
|
|
vector<uint256> vEraseQueue;
|
|
CDataStream vMsg(vRecv);
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
CTransaction tx;
|
|
vRecv >> tx;
|
|
|
|
CInv inv(MSG_TX, tx.GetHash());
|
|
pfrom->AddInventoryKnown(inv);
|
|
|
|
bool fMissingInputs = false;
|
|
if (tx.AcceptToMemoryPool(txdb, true, &fMissingInputs))
|
|
{
|
|
SyncWithWallets(tx, nullptr, true);
|
|
RelayTransaction(tx, inv.hash);
|
|
mapAlreadyAskedFor.erase(inv);
|
|
vWorkQueue.push_back(inv.hash);
|
|
vEraseQueue.push_back(inv.hash);
|
|
|
|
// Recursively process any orphan transactions that depended on this one
|
|
for (unsigned int i = 0; i < vWorkQueue.size(); i++)
|
|
{
|
|
uint256 hashPrev = vWorkQueue[i];
|
|
for (auto mi = mapOrphanTransactionsByPrev[hashPrev].begin();
|
|
mi != mapOrphanTransactionsByPrev[hashPrev].end();
|
|
++mi)
|
|
{
|
|
const uint256& orphanTxHash = *mi;
|
|
CTransaction& orphanTx = mapOrphanTransactions[orphanTxHash];
|
|
bool fMissingInputs2 = false;
|
|
|
|
if (orphanTx.AcceptToMemoryPool(txdb, true, &fMissingInputs2))
|
|
{
|
|
printf(" accepted orphan tx %s\n", orphanTxHash.ToString().substr(0,10).c_str());
|
|
SyncWithWallets(tx, nullptr, true);
|
|
RelayTransaction(orphanTx, orphanTxHash);
|
|
mapAlreadyAskedFor.erase(CInv(MSG_TX, orphanTxHash));
|
|
vWorkQueue.push_back(orphanTxHash);
|
|
vEraseQueue.push_back(orphanTxHash);
|
|
}
|
|
else if (!fMissingInputs2)
|
|
{
|
|
// invalid orphan
|
|
vEraseQueue.push_back(orphanTxHash);
|
|
printf(" removed invalid orphan tx %s\n", orphanTxHash.ToString().substr(0,10).c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (uint256 hash : vEraseQueue)
|
|
EraseOrphanTx(hash);
|
|
}
|
|
else if (fMissingInputs)
|
|
{
|
|
AddOrphanTx(tx);
|
|
|
|
// DoS prevention: do not allow mapOrphanTransactions to grow unbounded
|
|
unsigned int nEvicted = LimitOrphanTxSize(MAX_ORPHAN_TRANSACTIONS);
|
|
if (nEvicted > 0)
|
|
printf("mapOrphan overflow, removed %u tx\n", nEvicted);
|
|
}
|
|
if (tx.nDoS) pfrom->Misbehaving(tx.nDoS);
|
|
}
|
|
|
|
|
|
else if (strCommand == "block")
|
|
{
|
|
CBlock block;
|
|
vRecv >> block;
|
|
uint256 hashBlock = block.GetHash();
|
|
|
|
// Log every block during IBD (with throttling after first 100)
|
|
static int64_t nLastBlockLog = 0;
|
|
static int nBlocksReceived = 0;
|
|
nBlocksReceived++;
|
|
bool fLogThis = (nBlocksReceived <= 20) || (nBestHeight % 500 == 0) || !IsInitialBlockDownload() || (GetTime() - nLastBlockLog >= 5);
|
|
if (fLogThis) {
|
|
printf("IBD-DIAG: block received #%d hash=%s from=%s ourHeight=%d\n",
|
|
nBlocksReceived, hashBlock.ToString().substr(0,20).c_str(),
|
|
pfrom->addr.ToString().c_str(), nBestHeight);
|
|
nLastBlockLog = GetTime();
|
|
}
|
|
|
|
CInv inv(MSG_BLOCK, hashBlock);
|
|
pfrom->AddInventoryKnown(inv);
|
|
|
|
g_syncManager.TrackBlockDelivery(pfrom, hashBlock);
|
|
|
|
if (ProcessBlock(pfrom, &block))
|
|
{
|
|
mapAlreadyAskedFor.erase(inv);
|
|
|
|
if (IsInitialBlockDownload())
|
|
{
|
|
// Keep download window full after every accepted block
|
|
g_syncManager.QueueBlocksParallel();
|
|
|
|
static int nBlocksSinceRequest = 0;
|
|
if (++nBlocksSinceRequest >= 500)
|
|
{
|
|
nBlocksSinceRequest = 0;
|
|
// Pipeline refill: request from ALL connected full-node peers,
|
|
// not just the one that sent us this block. This spreads block
|
|
// download across multiple peers for better throughput.
|
|
// Also send getheaders to scout ahead faster than full blocks.
|
|
{
|
|
LOCK(cs_vNodes);
|
|
for (CNode* pnode : vNodes)
|
|
{
|
|
if (!pnode->fClient && pnode->nVersion != 0)
|
|
{
|
|
pnode->pindexLastGetBlocksBegin = nullptr;
|
|
pnode->PushGetBlocks(pindexBest, uint256(0));
|
|
pnode->pindexLastGetHeadersBegin = nullptr;
|
|
pnode->PushGetHeaders(pindexBest, uint256(0));
|
|
}
|
|
}
|
|
}
|
|
printf("IBD-DIAG: pipeline refill to all peers at height %d\n", nBestHeight);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("IBD-DIAG: ProcessBlock FAILED for block %s (height after prev=%d, DoS=%d)\n",
|
|
hashBlock.ToString().substr(0,20).c_str(), nBestHeight, block.nDoS);
|
|
}
|
|
|
|
if (block.nDoS) {
|
|
printf("IBD-DIAG: Misbehaving peer %s by %d\n",
|
|
pfrom->addr.ToString().c_str(), block.nDoS);
|
|
pfrom->Misbehaving(block.nDoS);
|
|
}
|
|
|
|
if (fSecMsgEnabled && !IsInitialBlockDownload())
|
|
SecureMsgScanBlock(block);
|
|
}
|
|
|
|
|
|
else if (strCommand == "cmpctblock")
|
|
{
|
|
CCompactBlock cmpctblock;
|
|
vRecv >> cmpctblock;
|
|
|
|
// Delegate to the standalone ProcessCompactBlock() which handles:
|
|
// - mempool short-ID matching with collision detection
|
|
// - merkle root verification before acceptance
|
|
// - partial block storage + getblocktxn request on missing txs
|
|
// - DoS scoring for malformed messages
|
|
ProcessCompactBlock(pfrom, cmpctblock);
|
|
}
|
|
|
|
|
|
else if (strCommand == "getblocktxn")
|
|
{
|
|
CBlockTxnRequest req;
|
|
vRecv >> req;
|
|
|
|
// Look up the block and send requested transactions
|
|
auto mi = mapBlockIndex.find(req.blockhash);
|
|
if (mi != mapBlockIndex.end())
|
|
{
|
|
CBlock block;
|
|
if (block.ReadFromDisk(mi->second))
|
|
{
|
|
CBlockTxnResponse resp;
|
|
resp.blockhash = req.blockhash;
|
|
for (uint16_t idx : req.vIndex)
|
|
{
|
|
if (idx < block.vtx.size())
|
|
resp.vTxn.push_back(block.vtx[idx]);
|
|
}
|
|
pfrom->PushMessage("blocktxn", resp);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
else if (strCommand == "blocktxn")
|
|
{
|
|
CBlockTxnResponse resp;
|
|
vRecv >> resp;
|
|
|
|
// Find the partial block awaiting these transactions
|
|
auto mi = mapPartialBlocks.find(resp.blockhash);
|
|
if (mi == mapPartialBlocks.end())
|
|
return true; // no longer need it
|
|
|
|
CPartialBlock& partial = mi->second;
|
|
unsigned int nFilled = 0;
|
|
auto itTxn = resp.vTxn.begin();
|
|
for (uint16_t idx : partial.setMissing)
|
|
{
|
|
if (itTxn == resp.vTxn.end())
|
|
break;
|
|
if (idx < partial.vTxFilled.size())
|
|
{
|
|
partial.vTxFilled[idx] = *itTxn;
|
|
nFilled++;
|
|
}
|
|
++itTxn;
|
|
}
|
|
partial.setMissing.clear(); // all filled now
|
|
|
|
// Reconstruct the complete block
|
|
CBlock block;
|
|
block.nVersion = partial.cmpctblock.nVersion;
|
|
block.hashPrevBlock = partial.cmpctblock.hashPrevBlock;
|
|
block.hashMerkleRoot = partial.cmpctblock.hashMerkleRoot;
|
|
block.nTime = partial.cmpctblock.nTime;
|
|
block.nBits = partial.cmpctblock.nBits;
|
|
block.nNonce = partial.cmpctblock.nNonce;
|
|
block.vchBlockSig = partial.cmpctblock.vchBlockSig;
|
|
block.vtx = partial.vTxFilled;
|
|
|
|
// Verify merkle root to detect corrupted or malicious blocktxn responses
|
|
uint256 hashMerkleComputed = block.BuildMerkleTree();
|
|
if (hashMerkleComputed != block.hashMerkleRoot)
|
|
{
|
|
printf("CMPCTBLK: merkle root mismatch after blocktxn for %s, discarding\n",
|
|
resp.blockhash.ToString().substr(0,20).c_str());
|
|
mapPartialBlocks.erase(mi);
|
|
pfrom->AskFor(CInv(MSG_BLOCK, resp.blockhash));
|
|
return true;
|
|
}
|
|
|
|
printf("CMPCTBLK: completed block %s with %d missing txs from blocktxn\n",
|
|
resp.blockhash.ToString().substr(0,20).c_str(), nFilled);
|
|
|
|
pfrom->nBlocksDelivered++;
|
|
if (nBestHeight > pfrom->nBestKnownHeight)
|
|
pfrom->nBestKnownHeight = nBestHeight;
|
|
ProcessBlock(pfrom, &block);
|
|
mapAlreadyAskedFor.erase(CInv(MSG_BLOCK, resp.blockhash));
|
|
mapPartialBlocks.erase(mi);
|
|
}
|
|
|
|
|
|
else if (strCommand == "getaddr")
|
|
{
|
|
// Don't return addresses older than nCutOff timestamp
|
|
int64_t nCutOff = GetTime() - (nNodeLifespan * 24 * 60 * 60);
|
|
pfrom->vAddrToSend.clear();
|
|
vector<CAddress> vAddr = addrman.GetAddr();
|
|
for (const CAddress &addr : vAddr)
|
|
if(addr.nTime > nCutOff)
|
|
pfrom->PushAddress(addr);
|
|
}
|
|
|
|
|
|
else if (strCommand == "mempool")
|
|
{
|
|
std::vector<uint256> vtxid;
|
|
mempool.queryHashes(vtxid);
|
|
vector<CInv> vInv;
|
|
for (unsigned int i = 0; i < vtxid.size(); i++) {
|
|
CInv inv(MSG_TX, vtxid[i]);
|
|
vInv.push_back(inv);
|
|
if (i == (MAX_INV_SZ - 1))
|
|
break;
|
|
}
|
|
if (vInv.size() > 0)
|
|
pfrom->PushMessage("inv", vInv);
|
|
}
|
|
|
|
|
|
else if (strCommand == "ping")
|
|
{
|
|
if (pfrom->nVersion > BIP0031_VERSION)
|
|
{
|
|
uint64_t nonce = 0;
|
|
vRecv >> nonce;
|
|
pfrom->PushMessage("pong", nonce);
|
|
}
|
|
}
|
|
|
|
|
|
else if (strCommand == "pong")
|
|
{
|
|
if (pfrom->nVersion > BIP0031_VERSION)
|
|
{
|
|
uint64_t nonce = 0;
|
|
vRecv >> nonce;
|
|
// Only accept pong if it matches our outstanding ping nonce
|
|
if (nonce != 0 && nonce == pfrom->nPingNonceSent) {
|
|
int64_t nRtt = GetTimeMicros() - pfrom->nPingUsecStart;
|
|
if (nRtt > 0) {
|
|
pfrom->nPingUsecTime = nRtt;
|
|
// Update rolling average block latency if not set
|
|
if (pfrom->nAvgBlockLatencyUs == 0)
|
|
pfrom->nAvgBlockLatencyUs = nRtt;
|
|
else
|
|
pfrom->nAvgBlockLatencyUs = (pfrom->nAvgBlockLatencyUs * 3 + nRtt) / 4;
|
|
}
|
|
pfrom->nPingNonceSent = 0;
|
|
pfrom->nPingUsecStart = 0;
|
|
pfrom->nPingRetryCount = 0;
|
|
if (fDebug)
|
|
printf("pong from %s: %.1fms\n", pfrom->addr.ToString().c_str(), (double)nRtt / 1000.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
else if (strCommand == "getsnap" || strCommand == "snap" ||
|
|
strCommand == "getsnapchunk" || strCommand == "snapchunk")
|
|
{
|
|
SnapshotNet::ProcessSnapshotMessage(pfrom, strCommand, vRecv);
|
|
}
|
|
|
|
|
|
else if (strCommand == "getwalletaddr")
|
|
{
|
|
// Peer is requesting our TRI receiving address for onion resolution.
|
|
// Respond with address + signature proving ownership.
|
|
if (!pwalletMain)
|
|
return true;
|
|
|
|
// Get our onion address to sign
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
std::string ourOnion = torMgr ? torMgr->GetWalletOnionAddress() : "";
|
|
if (ourOnion.empty())
|
|
{
|
|
CTorEmbedded* torEmbed = CTorEmbedded::GetInstance();
|
|
if (torEmbed)
|
|
ourOnion = torEmbed->GetOnionAddress();
|
|
}
|
|
if (ourOnion.empty())
|
|
return true; // Can't prove identity without onion address
|
|
|
|
// Get the default receiving address
|
|
CPubKey pubKey;
|
|
if (!pwalletMain->GetKeyFromPool(pubKey, true))
|
|
return true;
|
|
|
|
CTrianglesAddress addr(pubKey.GetID());
|
|
std::string strAddr = addr.ToString();
|
|
|
|
// Sign our onion address with the wallet key
|
|
CKeyID keyID;
|
|
addr.GetKeyID(keyID);
|
|
CKey key;
|
|
if (!pwalletMain->GetKey(keyID, key))
|
|
return true;
|
|
|
|
CDataStream ss(SER_GETHASH, 0);
|
|
ss << strMessageMagic;
|
|
ss << ourOnion;
|
|
|
|
std::vector<unsigned char> vchSig;
|
|
if (!key.SignCompact(Hash(ss.begin(), ss.end()), vchSig))
|
|
return true;
|
|
|
|
pfrom->PushMessage("walletaddr", strAddr, vchSig);
|
|
}
|
|
|
|
else if (strCommand == "walletaddr")
|
|
{
|
|
// Peer is responding with their TRI address + signature
|
|
std::string triAddr;
|
|
std::vector<unsigned char> vchSig;
|
|
vRecv >> triAddr >> vchSig;
|
|
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
if (torMgr)
|
|
torMgr->HandleWalletAddrResponse(pfrom, triAddr, vchSig);
|
|
}
|
|
|
|
else if (strCommand == "seeder")
|
|
{
|
|
// Receive seeder node announcement
|
|
std::string onionAddress;
|
|
int port;
|
|
vRecv >> onionAddress >> port;
|
|
|
|
std::string seederAddr = onionAddress + ":" + std::to_string(port);
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
if (torMgr && torMgr->IsTorEnabled())
|
|
{
|
|
torMgr->ConnectToSeederNode(seederAddr);
|
|
torMgr->UpdateSeederLastSeen(seederAddr);
|
|
}
|
|
}
|
|
|
|
else if (strCommand == "getseederlist")
|
|
{
|
|
// Peer is requesting our known seeder list
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
if (torMgr && torMgr->IsTorEnabled())
|
|
{
|
|
std::vector<std::string> seederList = torMgr->GetKnownSeederNodes();
|
|
pfrom->PushMessage("seederlist", seederList);
|
|
}
|
|
}
|
|
|
|
else if (strCommand == "seederlist")
|
|
{
|
|
// Receive seeder list from peer
|
|
std::vector<std::string> seederList;
|
|
vRecv >> seederList;
|
|
|
|
CTorV3Manager* torMgr = CTorV3Manager::GetInstance();
|
|
if (torMgr && torMgr->IsTorEnabled())
|
|
{
|
|
torMgr->HandleSeederListMessage(pfrom, seederList);
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
if (fSecMsgEnabled)
|
|
SecureMsgReceiveData(pfrom, strCommand, vRecv);
|
|
|
|
// Ignore unknown commands for extensibility
|
|
}
|
|
|
|
|
|
// Update the last seen time for this node's address
|
|
if (pfrom->fNetworkNode)
|
|
if (strCommand == "version" || strCommand == "addr" || strCommand == "inv" || strCommand == "getdata" || strCommand == "ping" || strCommand == "pong")
|
|
AddressCurrentlyConnected(pfrom->addr);
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ProcessMessages(CNode* pfrom)
|
|
{
|
|
//if (fDebug)
|
|
// printf("ProcessMessages(%u bytes)\n", vRecv.size());
|
|
|
|
//
|
|
// Message format
|
|
// (4) message start
|
|
// (12) command
|
|
// (4) size
|
|
// (4) checksum
|
|
// (x) data
|
|
//
|
|
bool fOk = true;
|
|
|
|
std::deque<CNetMessage>::iterator it = pfrom->vRecvMsg.begin();
|
|
while (!pfrom->fDisconnect && it != pfrom->vRecvMsg.end()) {
|
|
// Don't bother if send buffer is too full to respond anyway
|
|
if (pfrom->nSendSize >= SendBufferSize())
|
|
break;
|
|
|
|
// get next message
|
|
CNetMessage& msg = *it;
|
|
|
|
//if (fDebug)
|
|
// printf("ProcessMessages(message %u msgsz, %zu bytes, complete:%s)\n",
|
|
// msg.hdr.nMessageSize, msg.vRecv.size(),
|
|
// msg.complete() ? "Y" : "N");
|
|
|
|
// end, if an incomplete message is found
|
|
if (!msg.complete())
|
|
break;
|
|
|
|
// at this point, any failure means we can delete the current message
|
|
it++;
|
|
|
|
// Scan for message start
|
|
if (memcmp(msg.hdr.pchMessageStart, pchMessageStart, sizeof(pchMessageStart)) != 0) {
|
|
printf("\n\nPROCESSMESSAGE: INVALID MESSAGESTART\n\n");
|
|
fOk = false;
|
|
break;
|
|
}
|
|
|
|
// Read header
|
|
CMessageHeader& hdr = msg.hdr;
|
|
if (!hdr.IsValid())
|
|
{
|
|
printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
|
|
continue;
|
|
}
|
|
string strCommand = hdr.GetCommand();
|
|
|
|
// Message size
|
|
unsigned int nMessageSize = hdr.nMessageSize;
|
|
|
|
// Checksum
|
|
CDataStream& vRecv = msg.vRecv;
|
|
uint256 hash = Hash(vRecv.begin(), vRecv.begin() + nMessageSize);
|
|
unsigned int nChecksum = 0;
|
|
memcpy(&nChecksum, &hash, sizeof(nChecksum));
|
|
if (nChecksum != hdr.nChecksum)
|
|
{
|
|
printf("ProcessMessages(%s, %u bytes) : CHECKSUM ERROR nChecksum=%08x hdr.nChecksum=%08x\n",
|
|
strCommand.c_str(), nMessageSize, nChecksum, hdr.nChecksum);
|
|
continue;
|
|
}
|
|
|
|
// Process message
|
|
bool fRet = false;
|
|
try
|
|
{
|
|
{
|
|
LOCK(cs_main);
|
|
fRet = ProcessMessage(pfrom, strCommand, vRecv);
|
|
}
|
|
if (fShutdown)
|
|
return true;
|
|
}
|
|
catch (std::ios_base::failure& e)
|
|
{
|
|
if (strstr(e.what(), "end of data"))
|
|
{
|
|
// Allow exceptions from under-length message on vRecv
|
|
printf("ProcessMessages(%s, %u bytes) : Exception '%s' caught, normally caused by a message being shorter than its stated length\n", strCommand.c_str(), nMessageSize, e.what());
|
|
}
|
|
else if (strstr(e.what(), "size too large"))
|
|
{
|
|
// Allow exceptions from over-long size
|
|
printf("ProcessMessages(%s, %u bytes) : Exception '%s' caught\n", strCommand.c_str(), nMessageSize, e.what());
|
|
}
|
|
else
|
|
{
|
|
PrintExceptionContinue(&e, "ProcessMessages()");
|
|
}
|
|
}
|
|
catch (std::exception& e) {
|
|
PrintExceptionContinue(&e, "ProcessMessages()");
|
|
} catch (...) {
|
|
PrintExceptionContinue(nullptr, "ProcessMessages()");
|
|
}
|
|
|
|
if (!fRet && fDebug)
|
|
printf("ProcessMessage(%s, %u bytes) FAILED\n", strCommand.c_str(), nMessageSize);
|
|
}
|
|
|
|
// In case the connection got shut down, its receive buffer was wiped
|
|
if (!pfrom->fDisconnect)
|
|
pfrom->vRecvMsg.erase(pfrom->vRecvMsg.begin(), it);
|
|
|
|
return fOk;
|
|
}
|
|
|
|
|
|
bool SendMessages(CNode* pto, bool fSendTrickle)
|
|
{
|
|
TRY_LOCK(cs_main, lockMain);
|
|
if (lockMain) {
|
|
// Don't send anything until we get their version message
|
|
if (pto->nVersion == 0)
|
|
return true;
|
|
|
|
// Periodically clean up expired partial compact blocks (BIP152)
|
|
CleanupPartialBlocks();
|
|
|
|
// Keep-alive ping every 2 minutes (critical for Tor connections that
|
|
// can be silently dropped). Also measures round-trip latency.
|
|
{
|
|
bool fPingNeeded = false;
|
|
// Send ping every 2 minutes if no recent send activity
|
|
if (pto->nLastSend && GetTime() - pto->nLastSend > 120 && pto->ssSend.empty())
|
|
fPingNeeded = true;
|
|
// Also ping if we haven't sent one in 2 minutes regardless
|
|
if (pto->nPingUsecStart == 0 && GetTime() - pto->nTimeConnected > 120)
|
|
fPingNeeded = true;
|
|
if (pto->nPingUsecStart > 0 && GetTimeMicros() - pto->nPingUsecStart > 120 * 1000000)
|
|
fPingNeeded = true; // outstanding ping timed out, retry
|
|
|
|
if (fPingNeeded) {
|
|
// Check for dead peer: 3 consecutive unanswered pings = disconnect
|
|
if (pto->nPingNonceSent != 0 && pto->nPingUsecStart > 0) {
|
|
pto->nPingRetryCount++;
|
|
if (pto->nPingRetryCount >= 3) {
|
|
printf("ping timeout: %s (no pong for %d pings, %.1fs)\n",
|
|
pto->addr.ToString().c_str(), pto->nPingRetryCount,
|
|
(double)(GetTimeMicros() - pto->nPingUsecStart) / 1000000.0);
|
|
pto->fDisconnect = true;
|
|
}
|
|
}
|
|
if (!pto->fDisconnect) {
|
|
uint64_t nonce = 0;
|
|
while (nonce == 0)
|
|
RAND_bytes((unsigned char*)&nonce, sizeof(nonce));
|
|
pto->nPingNonceSent = nonce;
|
|
pto->nPingUsecStart = GetTimeMicros();
|
|
pto->PushMessage("ping", nonce);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resend wallet transactions that haven't gotten in a block yet
|
|
ResendWalletTransactions();
|
|
|
|
// Address refresh broadcast — every hour for small Tor-only networks
|
|
// (was 24 hours, but small networks need faster address propagation)
|
|
static int64_t nLastRebroadcast;
|
|
if (!IsInitialBlockDownload() && (GetTime() - nLastRebroadcast > 60 * 60))
|
|
{
|
|
{
|
|
LOCK(cs_vNodes);
|
|
for (CNode* pnode : vNodes)
|
|
{
|
|
// Periodically clear setAddrKnown to allow refresh broadcasts
|
|
if (nLastRebroadcast)
|
|
pnode->setAddrKnown.clear();
|
|
|
|
// Rebroadcast our address
|
|
if (!fNoListen)
|
|
{
|
|
CAddress addr = GetLocalAddress(&pnode->addr);
|
|
if (addr.IsRoutable())
|
|
pnode->PushAddress(addr);
|
|
}
|
|
|
|
// Periodically re-request addresses (every hour)
|
|
// Helps small networks discover all peers faster
|
|
if (!pnode->fGetAddr && pnode->fSuccessfullyConnected)
|
|
{
|
|
pnode->PushMessage("getaddr");
|
|
pnode->fGetAddr = true;
|
|
}
|
|
}
|
|
}
|
|
nLastRebroadcast = GetTime();
|
|
}
|
|
|
|
//
|
|
// Message: addr
|
|
//
|
|
if (fSendTrickle)
|
|
{
|
|
vector<CAddress> vAddr;
|
|
vAddr.reserve(pto->vAddrToSend.size());
|
|
for (const CAddress& addr : pto->vAddrToSend)
|
|
{
|
|
// returns true if wasn't already contained in the set
|
|
if (pto->setAddrKnown.insert(addr).second)
|
|
{
|
|
vAddr.push_back(addr);
|
|
// receiver rejects addr messages larger than 1000
|
|
if (vAddr.size() >= 1000)
|
|
{
|
|
pto->PushMessage("addr", vAddr);
|
|
vAddr.clear();
|
|
}
|
|
}
|
|
}
|
|
pto->vAddrToSend.clear();
|
|
if (!vAddr.empty())
|
|
pto->PushMessage("addr", vAddr);
|
|
}
|
|
|
|
|
|
//
|
|
// Message: inventory
|
|
//
|
|
vector<CInv> vInv;
|
|
vector<CInv> vInvWait;
|
|
{
|
|
LOCK(pto->cs_inventory);
|
|
vInv.reserve(pto->vInventoryToSend.size());
|
|
vInvWait.reserve(pto->vInventoryToSend.size());
|
|
for (const CInv& inv : pto->vInventoryToSend)
|
|
{
|
|
if (pto->setInventoryKnown.count(inv))
|
|
continue;
|
|
|
|
// trickle out tx inv to protect privacy
|
|
if (inv.type == MSG_TX && !fSendTrickle)
|
|
{
|
|
// 1/4 of tx invs blast to all immediately
|
|
static uint256 hashSalt;
|
|
if (hashSalt == 0)
|
|
hashSalt = GetRandHash();
|
|
uint256 hashRand = inv.hash ^ hashSalt;
|
|
hashRand = Hash(BEGIN(hashRand), END(hashRand));
|
|
bool fTrickleWait = ((hashRand & 3) != 0);
|
|
|
|
// always trickle our own transactions
|
|
if (!fTrickleWait)
|
|
{
|
|
CWalletTx wtx;
|
|
if (GetTransaction(inv.hash, wtx))
|
|
if (wtx.fFromMe)
|
|
fTrickleWait = true;
|
|
}
|
|
|
|
if (fTrickleWait)
|
|
{
|
|
vInvWait.push_back(inv);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// returns true if wasn't already contained in the set
|
|
if (pto->setInventoryKnown.insert(inv).second)
|
|
{
|
|
vInv.push_back(inv);
|
|
if (vInv.size() >= 1000)
|
|
{
|
|
pto->PushMessage("inv", vInv);
|
|
vInv.clear();
|
|
}
|
|
}
|
|
}
|
|
pto->vInventoryToSend = vInvWait;
|
|
}
|
|
if (!vInv.empty())
|
|
pto->PushMessage("inv", vInv);
|
|
|
|
|
|
//
|
|
// Periodic chain-tip sync: every 45 seconds, ask each peer if they
|
|
// have blocks we don't. On a small Tor-only network, transient
|
|
// partitions can cause forks that persist silently — this ensures
|
|
// nodes discover the longer chain even without explicit announcement.
|
|
//
|
|
if (!IsInitialBlockDownload() && !pto->fClient && pindexBest &&
|
|
GetTime() - pto->nLastTipCheck > 45)
|
|
{
|
|
pto->nLastTipCheck = GetTime();
|
|
pto->pindexLastGetBlocksBegin = nullptr; // reset dedup to force request
|
|
pto->PushGetBlocks(pindexBest, uint256(0));
|
|
}
|
|
|
|
//
|
|
// Slow-peer eviction: every 5 minutes during sync, identify the
|
|
// outbound peer with the fewest blocks delivered and disconnect it
|
|
// to free the slot for a potentially faster peer. This is critical
|
|
// on Tor networks with high latency variance.
|
|
//
|
|
if (nBestHeight < GetNumBlocksOfPeers() && !pto->fClient && !pto->fInbound)
|
|
{
|
|
static int64_t nLastEvictionCheck = 0;
|
|
if (GetTime() - nLastEvictionCheck > 5 * 60)
|
|
{
|
|
nLastEvictionCheck = GetTime();
|
|
CNode* pWorst = nullptr;
|
|
int nWorstBlocks = INT_MAX;
|
|
int nOutbound = 0;
|
|
{
|
|
LOCK(cs_vNodes);
|
|
for (CNode* pnode : vNodes)
|
|
{
|
|
if (pnode->fInbound || pnode->fDisconnect || pnode->fClient)
|
|
continue;
|
|
nOutbound++;
|
|
// Only consider peers connected for at least 3 minutes
|
|
if (GetTime() - pnode->nTimeConnected < 3 * 60)
|
|
continue;
|
|
if (pnode->nBlocksDelivered < nWorstBlocks)
|
|
{
|
|
nWorstBlocks = pnode->nBlocksDelivered;
|
|
pWorst = pnode;
|
|
}
|
|
}
|
|
// Only evict if we have at least 3 outbound peers and the worst
|
|
// peer has delivered significantly fewer blocks than average
|
|
if (pWorst && nOutbound >= 3 && nWorstBlocks == 0)
|
|
{
|
|
printf("PEER-EVICT: disconnecting slow peer %s (0 blocks delivered in %ds)\n",
|
|
pWorst->addr.ToString().c_str(),
|
|
(int)(GetTime() - pWorst->nTimeConnected));
|
|
pWorst->fDisconnect = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Stall detection: if we're still catching up and no new blocks for
|
|
// a while, re-request. Active during IBD (5s timeout) and also
|
|
// post-IBD when we're behind peers (30s timeout) to handle the case
|
|
// where IBD flips to false during a transient download gap.
|
|
//
|
|
if (!pto->fClient && nBestHeight < GetNumBlocksOfPeers())
|
|
{
|
|
static int64_t nLastBlockReceived = 0;
|
|
static int nLastHeight = 0;
|
|
static int64_t nLastStallLog = 0;
|
|
// Adaptive stall timeout: use peer's latency if known
|
|
int nStallTimeout;
|
|
if (pto->nAvgBlockLatencyUs > 0) {
|
|
// 5x average latency, clamped to 5-60 seconds
|
|
nStallTimeout = std::max(5, std::min(60, (int)(pto->nAvgBlockLatencyUs * 5 / 1000000)));
|
|
} else {
|
|
nStallTimeout = IsInitialBlockDownload() ? 10 : 30;
|
|
}
|
|
if (nBestHeight > nLastHeight) {
|
|
nLastHeight = nBestHeight;
|
|
nLastBlockReceived = GetTime();
|
|
} else if (nLastBlockReceived > 0 && GetTime() - nLastBlockReceived > nStallTimeout) {
|
|
if (GetTime() - nLastStallLog >= 15) { // log every 15s max
|
|
printf("SYNC-DIAG: STALL at height %d/%d for %ds (IBD=%d walk=%d), peer=%s askfor_queue=%d\n",
|
|
nBestHeight, GetNumBlocksOfPeers(),
|
|
(int)(GetTime() - nLastBlockReceived),
|
|
IsInitialBlockDownload(), nHighestInvWalk,
|
|
pto->addr.ToString().c_str(),
|
|
(int)pto->mapAskFor.size());
|
|
nLastStallLog = GetTime();
|
|
}
|
|
// During IBD, avoid falling back to legacy getblocks recovery
|
|
// anchored at pindexBest or a stale inv walk point. That path can
|
|
// repeatedly resolve the locator to the same low common ancestor
|
|
// on a weak peer set, which looks like a sync "freeze" near an
|
|
// early height even though the real bug is the recovery loop.
|
|
// Keep stall recovery header-driven instead so the planner tip
|
|
// advances from the newest known header state.
|
|
if (IsInitialBlockDownload())
|
|
{
|
|
pto->pindexLastGetHeadersBegin = nullptr;
|
|
|
|
uint256 hashLocatorTip = g_syncManager.GetBestHeader();
|
|
if (hashLocatorTip == 0 && nHighestInvWalk > nBestHeight &&
|
|
hashHighestInvWalk != 0 && mapBlockIndex.count(hashHighestInvWalk))
|
|
{
|
|
hashLocatorTip = hashHighestInvWalk;
|
|
}
|
|
|
|
unsigned int nRefilled = g_syncManager.RequestRefillAllPeers(
|
|
hashLocatorTip,
|
|
0,
|
|
"stall-recovery");
|
|
unsigned int nQueued = g_syncManager.QueueBlocksParallel();
|
|
|
|
printf("SYNC-DIAG: stall recovery used headers-first path (locator=%s, refillPeers=%u, queued=%u)\n",
|
|
hashLocatorTip.ToString().substr(0,20).c_str(),
|
|
nRefilled,
|
|
nQueued);
|
|
}
|
|
else
|
|
{
|
|
// Outside IBD, preserve the older walk-forward getblocks
|
|
// behavior since we're no longer building out a header planner.
|
|
pto->pindexLastGetBlocksBegin = nullptr;
|
|
if (nHighestInvWalk > nBestHeight && hashHighestInvWalk != 0 &&
|
|
mapBlockIndex.count(hashHighestInvWalk))
|
|
{
|
|
pto->PushGetBlocks(mapBlockIndex[hashHighestInvWalk], uint256(0));
|
|
printf("SYNC-DIAG: stall re-request from walk=%d (not best=%d)\n",
|
|
nHighestInvWalk, nBestHeight);
|
|
}
|
|
else
|
|
{
|
|
pto->PushGetBlocks(pindexBest, uint256(0));
|
|
}
|
|
|
|
pto->pindexLastGetHeadersBegin = nullptr;
|
|
pto->PushGetHeaders(pindexBest, uint256(0));
|
|
}
|
|
nLastBlockReceived = GetTime();
|
|
}
|
|
}
|
|
|
|
// Per-peer IBD getheaders heartbeat and block-planner cadence.
|
|
// Logic lives in CSyncManager::Tick — see syncmanager.cpp.
|
|
g_syncManager.Tick(pto, nHighestInvWalk, hashHighestInvWalk);
|
|
|
|
//
|
|
// Message: getdata
|
|
//
|
|
// Periodic IBD status
|
|
if (IsInitialBlockDownload()) {
|
|
static int64_t nLastStatus = 0;
|
|
if (GetTime() - nLastStatus >= 15) {
|
|
printf("IBD-DIAG: STATUS height=%d plannerHeight=%d plannerDepth=%u inflight=%u peers=%d askfor_queued=%d orphans=%d\n",
|
|
nBestHeight,
|
|
g_syncManager.GetPlannerHeight(),
|
|
g_syncManager.GetPlannerDepth(),
|
|
g_syncManager.CountInFlight(),
|
|
(int)vNodes.size(),
|
|
(int)pto->mapAskFor.size(),
|
|
(int)mapOrphanBlocks.size());
|
|
nLastStatus = GetTime();
|
|
}
|
|
}
|
|
|
|
vector<CInv> vGetData;
|
|
int64_t nNow = GetTime() * 1000000;
|
|
auto txdb_holder = MakeChainDB("r"); CTxDBBase& txdb = *txdb_holder;
|
|
// During IBD, send larger getdata batches since PoS blocks are small
|
|
// and the bottleneck is round-trip latency, not bandwidth.
|
|
unsigned int nGetDataBatchSize = IsInitialBlockDownload() ? 4000 : 1000;
|
|
while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow)
|
|
{
|
|
const CInv& inv = (*pto->mapAskFor.begin()).second;
|
|
if (!AlreadyHave(txdb, inv))
|
|
{
|
|
if (fDebugNet)
|
|
printf("sending getdata: %s\n", inv.ToString().c_str());
|
|
vGetData.push_back(inv);
|
|
if (vGetData.size() >= nGetDataBatchSize)
|
|
{
|
|
pto->PushMessage("getdata", vGetData);
|
|
vGetData.clear();
|
|
}
|
|
mapAlreadyAskedFor[inv] = nNow;
|
|
}
|
|
pto->mapAskFor.erase(pto->mapAskFor.begin());
|
|
}
|
|
if (!vGetData.empty())
|
|
pto->PushMessage("getdata", vGetData);
|
|
|
|
if (fSecMsgEnabled)
|
|
SecureMsgSendData(pto, fSendTrickle); // should be in cs_main?
|
|
}
|
|
return true;
|
|
}
|