UTXO database model + startup performance optimizations
Replace per-transaction CTxIndex spent tracking with per-output UTXO database (CUtxoEntry). ConnectBlock writes/erases UTXOs as blocks are processed. FetchInputs reads directly from UTXO DB instead of deserializing full transactions from disk. Persist nChainTrust in block index (dbformat v3) to skip expensive recalculation on every startup. Only populate setStakeSeen for last 500 blocks instead of all 2M+. Lazy fallback to old CTxIndex path for databases upgrading from pre-UTXO format - no big-bang migration required. Fixes pre-existing bugs in introdialog.cpp (extra brace) and net_bootstrap.cpp (namespace extern). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+325
-174
@@ -682,12 +682,15 @@ bool CTransaction::AreInputsStandard(const MapPrevTx& mapInputs) const
|
||||
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
const CTxOut& prev = GetOutputFor(vin[i], mapInputs);
|
||||
MapPrevTx::const_iterator mi = mapInputs.find(vin[i].prevout);
|
||||
if (mi == mapInputs.end())
|
||||
return false;
|
||||
const CUtxoEntry& entry = mi->second;
|
||||
|
||||
vector<vector<unsigned char> > vSolutions;
|
||||
txnouttype whichType;
|
||||
// get the scriptPubKey corresponding to this input:
|
||||
const CScript& prevScript = prev.scriptPubKey;
|
||||
const CScript& prevScript = entry.scriptPubKey;
|
||||
if (!Solver(prevScript, whichType, vSolutions))
|
||||
return false;
|
||||
int nArgsExpected = ScriptSigArgsExpected(whichType, vSolutions);
|
||||
@@ -951,9 +954,9 @@ bool CTxMemPool::accept(CTxDB& txdb, CTransaction &tx, bool fCheckInputs,
|
||||
if (fCheckInputs)
|
||||
{
|
||||
MapPrevTx mapInputs;
|
||||
map<uint256, CTxIndex> mapUnused;
|
||||
MapPrevTx mapEmpty; // no pending UTXOs for mempool acceptance
|
||||
bool fInvalid = false;
|
||||
if (!tx.FetchInputs(txdb, mapUnused, false, false, mapInputs, fInvalid))
|
||||
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());
|
||||
@@ -1007,7 +1010,7 @@ bool CTxMemPool::accept(CTxDB& txdb, CTransaction &tx, bool fCheckInputs,
|
||||
|
||||
// Check against previous transactions
|
||||
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, mapUnused, CDiskTxPos(1,1,1), pindexBest, false, false))
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, pindexBest, false, false))
|
||||
{
|
||||
return error("CTxMemPool::accept() : ConnectInputs failed %s", hash.ToString().substr(0,10).c_str());
|
||||
}
|
||||
@@ -1529,41 +1532,16 @@ void CBlock::UpdateTime(const CBlockIndex* pindexPrev)
|
||||
|
||||
bool CTransaction::DisconnectInputs(CTxDB& txdb)
|
||||
{
|
||||
// Relinquish previous transactions' spent pointers
|
||||
if (!IsCoinBase())
|
||||
{
|
||||
for (const CTxIn& txin : vin)
|
||||
{
|
||||
COutPoint prevout = txin.prevout;
|
||||
|
||||
// Get prev txindex from disk
|
||||
CTxIndex txindex;
|
||||
if (!txdb.ReadTxIndex(prevout.hash, txindex))
|
||||
return error("DisconnectInputs() : ReadTxIndex failed");
|
||||
|
||||
if (prevout.n >= txindex.vSpent.size())
|
||||
return error("DisconnectInputs() : prevout.n out of range");
|
||||
|
||||
// Mark outpoint as not spent
|
||||
txindex.vSpent[prevout.n].SetNull();
|
||||
|
||||
// Write back
|
||||
if (!txdb.UpdateTxIndex(prevout.hash, txindex))
|
||||
return error("DisconnectInputs() : UpdateTxIndex failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove transaction from index
|
||||
// This can fail if a duplicate of this transaction was in a chain that got
|
||||
// reorganized away. This is only possible if this transaction was completely
|
||||
// spent, so erasing it would be a no-op anyway.
|
||||
// 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(CTxDB& txdb, const map<uint256, CTxIndex>& mapTestPool,
|
||||
bool CTransaction::FetchInputs(CTxDB& 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
|
||||
@@ -1578,61 +1556,96 @@ bool CTransaction::FetchInputs(CTxDB& txdb, const map<uint256, CTxIndex>& mapTes
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
COutPoint prevout = vin[i].prevout;
|
||||
if (inputsRet.count(prevout.hash))
|
||||
if (inputsRet.count(prevout))
|
||||
continue; // Got it already
|
||||
|
||||
// Read txindex
|
||||
CTxIndex& txindex = inputsRet[prevout.hash].first;
|
||||
bool fFound = true;
|
||||
if ((fBlock || fMiner) && mapTestPool.count(prevout.hash))
|
||||
// Check pending UTXOs from earlier transactions in the same block
|
||||
MapPrevTx::const_iterator mi = mapPendingUtxos.find(prevout);
|
||||
if (mi != mapPendingUtxos.end())
|
||||
{
|
||||
// Get txindex from current proposed changes
|
||||
txindex = mapTestPool.find(prevout.hash)->second;
|
||||
inputsRet[prevout] = mi->second;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read txindex from txdb
|
||||
fFound = txdb.ReadTxIndex(prevout.hash, txindex);
|
||||
}
|
||||
if (!fFound && (fBlock || fMiner))
|
||||
return fMiner ? false : error("FetchInputs() : %s prev tx %s index entry not found", GetHash().ToString().substr(0,10).c_str(), prevout.hash.ToString().substr(0,10).c_str());
|
||||
|
||||
// Read txPrev
|
||||
CTransaction& txPrev = inputsRet[prevout.hash].second;
|
||||
if (!fFound || txindex.pos == CDiskTxPos(1,1,1))
|
||||
// Read from UTXO database
|
||||
CUtxoEntry entry;
|
||||
if (txdb.ReadUtxo(prevout.hash, prevout.n, entry))
|
||||
{
|
||||
// Get prev tx from single transactions in memory
|
||||
inputsRet[prevout] = entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lazy fallback: try old CTxIndex path (for databases upgrading from pre-UTXO format)
|
||||
{
|
||||
CTxIndex txindex;
|
||||
if (txdb.ReadTxIndex(prevout.hash, txindex))
|
||||
{
|
||||
LOCK(mempool.cs);
|
||||
if (!mempool.exists(prevout.hash))
|
||||
return error("FetchInputs() : %s mempool Tx prev not found %s", GetHash().ToString().substr(0,10).c_str(), prevout.hash.ToString().substr(0,10).c_str());
|
||||
txPrev = mempool.lookup(prevout.hash);
|
||||
}
|
||||
if (!fFound)
|
||||
txindex.vSpent.resize(txPrev.vout.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get prev tx from disk
|
||||
if (!txPrev.ReadFromDisk(txindex.pos))
|
||||
return error("FetchInputs() : %s ReadFromDisk prev tx %s failed", GetHash().ToString().substr(0,10).c_str(), prevout.hash.ToString().substr(0,10).c_str());
|
||||
}
|
||||
}
|
||||
CTransaction txPrev;
|
||||
if (txPrev.ReadFromDisk(txindex.pos))
|
||||
{
|
||||
if (prevout.n < txPrev.vout.size())
|
||||
{
|
||||
CUtxoEntry backfill;
|
||||
backfill.nValue = txPrev.vout[prevout.n].nValue;
|
||||
backfill.scriptPubKey = txPrev.vout[prevout.n].scriptPubKey;
|
||||
backfill.fCoinBase = txPrev.IsCoinBase();
|
||||
backfill.fCoinStake = txPrev.IsCoinStake();
|
||||
backfill.nTxTime = txPrev.nTime;
|
||||
backfill.nHeight = 0; // conservative default
|
||||
|
||||
// Make sure all prevout.n indexes are valid:
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
const COutPoint prevout = vin[i].prevout;
|
||||
assert(inputsRet.count(prevout.hash) != 0);
|
||||
const CTxIndex& txindex = inputsRet[prevout.hash].first;
|
||||
const CTransaction& txPrev = inputsRet[prevout.hash].second;
|
||||
if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size())
|
||||
{
|
||||
// Revisit this if/when transaction replacement is implemented and allows
|
||||
// adding inputs:
|
||||
fInvalid = true;
|
||||
return DoS(100, error("FetchInputs() : %s prevout.n out of range %d %" PRIszu " %" PRIszu " prev tx %s\n%s", GetHash().ToString().substr(0,10).c_str(), prevout.n, txPrev.vout.size(), txindex.vSpent.size(), prevout.hash.ToString().substr(0,10).c_str(), txPrev.ToString().c_str()));
|
||||
// Try to recover exact block height from block index
|
||||
CBlock blockHeader;
|
||||
if (blockHeader.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
|
||||
{
|
||||
std::map<uint256, CBlockIndex*>::iterator bmi = mapBlockIndex.find(blockHeader.GetHash());
|
||||
if (bmi != mapBlockIndex.end())
|
||||
backfill.nHeight = bmi->second->nHeight;
|
||||
}
|
||||
|
||||
// Check if this output was already spent (vSpent in old format)
|
||||
if (prevout.n < txindex.vSpent.size() && !txindex.vSpent[prevout.n].IsNull())
|
||||
{
|
||||
// Already spent — don't return it as available
|
||||
}
|
||||
else
|
||||
{
|
||||
// Backfill to UTXO DB for future lookups
|
||||
txdb.WriteUtxo(prevout.hash, prevout.n, backfill);
|
||||
inputsRet[prevout] = backfill;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not in UTXO DB or old index — 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;
|
||||
@@ -1640,15 +1653,11 @@ bool CTransaction::FetchInputs(CTxDB& txdb, const map<uint256, CTxIndex>& mapTes
|
||||
|
||||
const CTxOut& CTransaction::GetOutputFor(const CTxIn& input, const MapPrevTx& inputs) const
|
||||
{
|
||||
MapPrevTx::const_iterator mi = inputs.find(input.prevout.hash);
|
||||
if (mi == inputs.end())
|
||||
throw std::runtime_error("CTransaction::GetOutputFor() : prevout.hash not found");
|
||||
|
||||
const CTransaction& txPrev = (mi->second).second;
|
||||
if (input.prevout.n >= txPrev.vout.size())
|
||||
throw std::runtime_error("CTransaction::GetOutputFor() : prevout.n out of range");
|
||||
|
||||
return txPrev.vout[input.prevout.n];
|
||||
// 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
|
||||
@@ -1659,10 +1668,12 @@ int64_t CTransaction::GetValueIn(const MapPrevTx& inputs) const
|
||||
int64_t nResult = 0;
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
nResult += GetOutputFor(vin[i], inputs).nValue;
|
||||
MapPrevTx::const_iterator mi = inputs.find(vin[i].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
|
||||
@@ -1673,20 +1684,22 @@ unsigned int CTransaction::GetP2SHSigOpCount(const MapPrevTx& inputs) const
|
||||
unsigned int nSigOps = 0;
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
const CTxOut& prevout = GetOutputFor(vin[i], inputs);
|
||||
if (prevout.scriptPubKey.IsPayToScriptHash())
|
||||
nSigOps += prevout.scriptPubKey.GetSigOpCount(vin[i].scriptSig);
|
||||
MapPrevTx::const_iterator mi = inputs.find(vin[i].prevout);
|
||||
if (mi == inputs.end())
|
||||
continue;
|
||||
const CScript& scriptPubKey = mi->second.scriptPubKey;
|
||||
if (scriptPubKey.IsPayToScriptHash())
|
||||
nSigOps += scriptPubKey.GetSigOpCount(vin[i].scriptSig);
|
||||
}
|
||||
return nSigOps;
|
||||
}
|
||||
|
||||
bool CTransaction::ConnectInputs(CTxDB& txdb, MapPrevTx inputs, map<uint256, CTxIndex>& mapTestPool, const CDiskTxPos& posThisTx,
|
||||
bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
|
||||
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner)
|
||||
{
|
||||
// Take over previous transactions' spent pointers
|
||||
// fBlock is true when this is called from AcceptBlock when a new best-block is added to the blockchain
|
||||
// fMiner is true when called from the internal triangles miner
|
||||
// ... both are false when called from CTransaction::AcceptToMemoryPool
|
||||
// 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;
|
||||
@@ -1694,64 +1707,44 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, MapPrevTx inputs, map<uint256, CTx
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
COutPoint prevout = vin[i].prevout;
|
||||
assert(inputs.count(prevout.hash) > 0);
|
||||
CTxIndex& txindex = inputs[prevout.hash].first;
|
||||
CTransaction& txPrev = inputs[prevout.hash].second;
|
||||
|
||||
if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size())
|
||||
return DoS(100, error("ConnectInputs() : %s prevout.n out of range %d %" PRIszu " %" PRIszu " prev tx %s\n%s", GetHash().ToString().substr(0,10).c_str(), prevout.n, txPrev.vout.size(), txindex.vSpent.size(), prevout.hash.ToString().substr(0,10).c_str(), txPrev.ToString().c_str()));
|
||||
MapPrevTx::const_iterator 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 prev is coinbase or coinstake, check that it's matured
|
||||
if (txPrev.IsCoinBase() || txPrev.IsCoinStake())
|
||||
for (const CBlockIndex* pindex = pindexBlock; pindex && pindexBlock->nHeight - pindex->nHeight < nCoinbaseMaturity; pindex = pindex->pprev)
|
||||
if (pindex->nBlockPos == txindex.pos.nBlockPos && pindex->nFile == txindex.pos.nFile)
|
||||
return error("ConnectInputs() : tried to spend %s at depth %d", txPrev.IsCoinBase() ? "coinbase" : "coinstake", pindexBlock->nHeight - pindex->nHeight);
|
||||
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);
|
||||
}
|
||||
|
||||
// triangles: check transaction timestamp
|
||||
if (txPrev.nTime > nTime)
|
||||
if (entry.nTxTime > nTime)
|
||||
return DoS(100, error("ConnectInputs() : transaction timestamp earlier than input transaction"));
|
||||
|
||||
// Check for negative or overflow input values
|
||||
nValueIn += txPrev.vout[prevout.n].nValue;
|
||||
if (!MoneyRange(txPrev.vout[prevout.n].nValue) || !MoneyRange(nValueIn))
|
||||
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.
|
||||
for (unsigned int i = 0; i < vin.size(); i++)
|
||||
{
|
||||
COutPoint prevout = vin[i].prevout;
|
||||
assert(inputs.count(prevout.hash) > 0);
|
||||
CTxIndex& txindex = inputs[prevout.hash].first;
|
||||
CTransaction& txPrev = inputs[prevout.hash].second;
|
||||
|
||||
// Check for conflicts (double-spend)
|
||||
// This doesn't trigger the DoS code on purpose; if it did, it would make it easier
|
||||
// for an attacker to attempt to split the network.
|
||||
if (!txindex.vSpent[prevout.n].IsNull())
|
||||
return fMiner ? false : error("ConnectInputs() : %s prev tx already used at %s", GetHash().ToString().substr(0,10).c_str(), txindex.vSpent[prevout.n].ToString().c_str());
|
||||
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())))
|
||||
{
|
||||
// Verify signature
|
||||
if (!VerifySignature(txPrev, *this, i, 0))
|
||||
{
|
||||
return DoS(100,error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,10).c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
// Mark outpoints as spent
|
||||
txindex.vSpent[prevout.n] = posThisTx;
|
||||
|
||||
// Write back
|
||||
if (fBlock || fMiner)
|
||||
{
|
||||
mapTestPool[prevout.hash] = txindex;
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1862,6 +1855,45 @@ bool CBlock::DisconnectBlock(CTxDB& txdb, CBlockIndex* pindex)
|
||||
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.nHeight = 0; // approximation; exact height not critical for restored UTXOs
|
||||
utxo.scriptPubKey = prevout.scriptPubKey;
|
||||
utxo.fCoinBase = txPrev.IsCoinBase();
|
||||
utxo.fCoinStake = txPrev.IsCoinStake();
|
||||
utxo.nTxTime = txPrev.nTime;
|
||||
txdb.WriteUtxo(txin.prevout.hash, txin.prevout.n, utxo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Undo address index entries for this block
|
||||
if (fAddressIndex)
|
||||
{
|
||||
@@ -1966,7 +1998,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
else
|
||||
nTxPos = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION) - (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(vtx.size());
|
||||
|
||||
map<uint256, CTxIndex> mapQueuedChanges;
|
||||
map<uint256, CTxIndex> mapQueuedChanges; // tx position index (for getrawtransaction)
|
||||
MapPrevTx mapPendingUtxos; // in-block UTXO tracking
|
||||
int64_t nFees = 0;
|
||||
int64_t nValueIn = 0;
|
||||
int64_t nValueOut = 0;
|
||||
@@ -1980,33 +2013,43 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
if (!fJustCheck)
|
||||
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
// Fast path: below checkpoint, skip all input validation and spent-tracking.
|
||||
// Just record where each transaction lives on disk (txindex).
|
||||
// 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)
|
||||
{
|
||||
mapQueuedChanges[hashTx] = CTxIndex(posThisTx, tx.vout.size());
|
||||
// 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)
|
||||
|
||||
// Do not allow blocks that contain transactions which 'overwrite' older transactions,
|
||||
// unless those are already completely spent.
|
||||
// If such overwrites are allowed, coinbases and transactions depending upon those
|
||||
// can be duplicated to remove the ability to spend the first instance -- even after
|
||||
// being sent to another address.
|
||||
// See BIP30 and http://r6.ca/blog/20120206T005236Z.html for more information.
|
||||
// This logic is not necessary for memory pool transactions, as AcceptToMemoryPool
|
||||
// already refuses previously-known transaction ids entirely.
|
||||
// This rule was originally applied all blocks whose timestamp was after March 15, 2012, 0:00 UTC.
|
||||
// Now that the whole chain is irreversibly beyond that time it is applied to all blocks except the
|
||||
// two in the chain that violate it. This prevents exploiting the issue against nodes in their
|
||||
// initial block download.
|
||||
CTxIndex txindexOld;
|
||||
if (txdb.ReadTxIndex(hashTx, txindexOld)) {
|
||||
for (CDiskTxPos &pos : txindexOld.vSpent)
|
||||
if (pos.IsNull())
|
||||
return false;
|
||||
// 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();
|
||||
@@ -2019,7 +2062,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
else
|
||||
{
|
||||
bool fInvalid;
|
||||
if (!tx.FetchInputs(txdb, mapQueuedChanges, true, false, mapInputs, fInvalid))
|
||||
if (!tx.FetchInputs(txdb, mapPendingUtxos, true, false, mapInputs, fInvalid))
|
||||
return false;
|
||||
|
||||
// Add in sigops done by pay-to-script-hash inputs;
|
||||
@@ -2038,11 +2081,29 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
if (tx.IsCoinStake())
|
||||
nStakeReward = nTxValueOut - nTxValueIn;
|
||||
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, mapQueuedChanges, posThisTx, pindex, true, false))
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, pindex, true, false))
|
||||
return false;
|
||||
}
|
||||
|
||||
mapQueuedChanges[hashTx] = CTxIndex(posThisTx, tx.vout.size());
|
||||
// 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)
|
||||
@@ -2086,6 +2147,42 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
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 (unsigned int i = 0; i < vtx.size(); i++)
|
||||
{
|
||||
const CTransaction& tx = vtx[i];
|
||||
uint256 hashTx = tx.GetHash();
|
||||
|
||||
// Add new outputs to UTXO set
|
||||
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");
|
||||
}
|
||||
|
||||
// Erase spent inputs from UTXO set
|
||||
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)
|
||||
{
|
||||
@@ -2387,11 +2484,22 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
|
||||
// Log every 5000 blocks during sync, every block once caught up
|
||||
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
||||
printf("SetBestChain: new best=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s\n",
|
||||
{
|
||||
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());
|
||||
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str(),
|
||||
dRate);
|
||||
nLastLogTime = nNow;
|
||||
nLastLogHeight = nBestHeight;
|
||||
}
|
||||
|
||||
if (fDebug)
|
||||
printf("Stake checkpoint: %x\n", pindexBest->nStakeModifierChecksum);
|
||||
@@ -2491,26 +2599,47 @@ bool CTransaction::GetCoinAge(CTxDB& txdb, uint64_t& nCoinAge) const
|
||||
|
||||
for (const CTxIn& txin : vin)
|
||||
{
|
||||
// First try finding the previous transaction in database
|
||||
CTransaction txPrev;
|
||||
CTxIndex txindex;
|
||||
if (!txPrev.ReadFromDisk(txdb, txin.prevout, txindex))
|
||||
continue; // previous transaction not in main chain
|
||||
if (nTime < txPrev.nTime)
|
||||
// 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
|
||||
// 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 = txPrev.vout[txin.prevout.n].nValue;
|
||||
bnCentSecond += CBigNum(nValueIn) * (nTime-txPrev.nTime) / CENT;
|
||||
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 - txPrev.nTime, bnCentSecond.ToString().c_str());
|
||||
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);
|
||||
@@ -3579,15 +3708,37 @@ bool FastImportBlockFile()
|
||||
// Write block index to batch
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
||||
|
||||
// Build tx index entries
|
||||
// Build tx index + UTXO entries
|
||||
unsigned int nTxPos = nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
|
||||
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(block.vtx.size());
|
||||
for (unsigned int i = 0; i < block.vtx.size(); i++)
|
||||
{
|
||||
const CTransaction& tx = block.vtx[i];
|
||||
uint256 hashTx = tx.GetHash();
|
||||
CDiskTxPos posThisTx(1, nBlockPos, nTxPos);
|
||||
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
|
||||
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
|
||||
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
// UTXO entries
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
|
||||
}
|
||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||
{
|
||||
if (!tx.vout[k].IsEmpty())
|
||||
{
|
||||
CUtxoEntry utxo;
|
||||
utxo.nValue = tx.vout[k].nValue;
|
||||
utxo.nHeight = pindexNew->nHeight;
|
||||
utxo.scriptPubKey = tx.vout[k].scriptPubKey;
|
||||
utxo.fCoinBase = tx.IsCoinBase();
|
||||
utxo.fCoinStake = tx.IsCoinStake();
|
||||
utxo.nTxTime = tx.nTime;
|
||||
txdb.WriteUtxo(hashTx, k, utxo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update best chain
|
||||
|
||||
+72
-23
@@ -431,7 +431,51 @@ enum GetMinFee_mode
|
||||
GMF_SEND,
|
||||
};
|
||||
|
||||
typedef std::map<uint256, std::pair<CTxIndex, CTransaction> > MapPrevTx;
|
||||
/** A single unspent transaction output entry in the UTXO database.
|
||||
* Keyed by (txhash, output_index). Erased when spent.
|
||||
*/
|
||||
class CUtxoEntry
|
||||
{
|
||||
public:
|
||||
int64_t nValue; // output value in satoshis
|
||||
int nHeight; // block height where output was created
|
||||
CScript scriptPubKey; // output script (needed for sig verification)
|
||||
bool fCoinBase; // from a coinbase transaction
|
||||
bool fCoinStake; // from a coinstake transaction
|
||||
unsigned int nTxTime; // transaction timestamp (needed for PoS coin age)
|
||||
|
||||
CUtxoEntry()
|
||||
{
|
||||
SetNull();
|
||||
}
|
||||
|
||||
IMPLEMENT_SERIALIZE
|
||||
(
|
||||
READWRITE(nValue);
|
||||
READWRITE(nHeight);
|
||||
READWRITE(scriptPubKey);
|
||||
READWRITE(fCoinBase);
|
||||
READWRITE(fCoinStake);
|
||||
READWRITE(nTxTime);
|
||||
)
|
||||
|
||||
void SetNull()
|
||||
{
|
||||
nValue = -1;
|
||||
nHeight = 0;
|
||||
scriptPubKey.clear();
|
||||
fCoinBase = false;
|
||||
fCoinStake = false;
|
||||
nTxTime = 0;
|
||||
}
|
||||
|
||||
bool IsNull() const
|
||||
{
|
||||
return (nValue == -1);
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::map<COutPoint, CUtxoEntry> MapPrevTx;
|
||||
|
||||
/** The basic transaction that is broadcasted on the network and contained in
|
||||
* blocks. A transaction can contain multiple inputs and outputs.
|
||||
@@ -672,32 +716,28 @@ public:
|
||||
bool ReadFromDisk(COutPoint prevout);
|
||||
bool DisconnectInputs(CTxDB& txdb);
|
||||
|
||||
/** Fetch from memory and/or disk. inputsRet keys are transaction hashes.
|
||||
/** Fetch UTXO entries for all inputs from the UTXO database or mempool.
|
||||
|
||||
@param[in] txdb Transaction database
|
||||
@param[in] mapTestPool List of pending changes to the transaction index database
|
||||
@param[in] fBlock True if being called to add a new best-block to the chain
|
||||
@param[in] fMiner True if being called by CreateNewBlock
|
||||
@param[out] inputsRet Pointers to this transaction's inputs
|
||||
@param[out] fInvalid returns true if transaction is invalid
|
||||
@return Returns true if all inputs are in txdb or mapTestPool
|
||||
@param[in] txdb Transaction database
|
||||
@param[in] mapPendingUtxos UTXOs created by earlier transactions in the same block
|
||||
@param[in] fBlock True if being called to add a new best-block to the chain
|
||||
@param[in] fMiner True if being called by CreateNewBlock
|
||||
@param[out] inputsRet UTXO entries for this transaction's inputs (keyed by COutPoint)
|
||||
@param[out] fInvalid returns true if transaction is invalid
|
||||
@return Returns true if all inputs are found
|
||||
*/
|
||||
bool FetchInputs(CTxDB& txdb, const std::map<uint256, CTxIndex>& mapTestPool,
|
||||
bool FetchInputs(CTxDB& txdb, const MapPrevTx& mapPendingUtxos,
|
||||
bool fBlock, bool fMiner, MapPrevTx& inputsRet, bool& fInvalid);
|
||||
|
||||
/** Sanity check previous transactions, then, if all checks succeed,
|
||||
mark them as spent by this transaction.
|
||||
/** Validate inputs against UTXO entries and verify signatures.
|
||||
|
||||
@param[in] inputs Previous transactions (from FetchInputs)
|
||||
@param[out] mapTestPool Keeps track of inputs that need to be updated on disk
|
||||
@param[in] posThisTx Position of this transaction on disk
|
||||
@param[in] pindexBlock
|
||||
@param[in] fBlock true if called from ConnectBlock
|
||||
@param[in] fMiner true if called from CreateNewBlock
|
||||
@param[in] inputs UTXO entries for inputs (from FetchInputs)
|
||||
@param[in] pindexBlock Block being connected
|
||||
@param[in] fBlock true if called from ConnectBlock
|
||||
@param[in] fMiner true if called from CreateNewBlock
|
||||
@return Returns true if all checks succeed
|
||||
*/
|
||||
bool ConnectInputs(CTxDB& txdb, MapPrevTx inputs,
|
||||
std::map<uint256, CTxIndex>& mapTestPool, const CDiskTxPos& posThisTx,
|
||||
bool ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
|
||||
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner);
|
||||
bool ClientConnectInputs();
|
||||
bool CheckTransaction() const;
|
||||
@@ -771,9 +811,10 @@ public:
|
||||
|
||||
|
||||
|
||||
/** A txdb record that contains the disk location of a transaction and the
|
||||
* locations of transactions that spend its outputs. vSpent is really only
|
||||
* used as a flag, but having the location is very helpful for debugging.
|
||||
/** A txdb record that contains the disk location of a transaction.
|
||||
* Used for getrawtransaction and wallet position lookups.
|
||||
* vSpent is legacy (kept for serialization compat) — spent state is
|
||||
* tracked by the UTXO set (CUtxoEntry) since dbformat=3.
|
||||
*/
|
||||
class CTxIndex
|
||||
{
|
||||
@@ -1364,6 +1405,10 @@ public:
|
||||
uint256 hashPrev;
|
||||
uint256 hashNext;
|
||||
|
||||
// When true, nChainTrust is included in the on-disk serialization.
|
||||
// Set by LoadBlockIndex based on the DB format version before deserializing.
|
||||
static bool fSerializeChainTrust;
|
||||
|
||||
CDiskBlockIndex()
|
||||
{
|
||||
hashPrev = 0;
|
||||
@@ -1411,6 +1456,10 @@ public:
|
||||
READWRITE(nBits);
|
||||
READWRITE(nNonce);
|
||||
READWRITE(blockHash);
|
||||
|
||||
// DB format v2+: persist chain trust to skip expensive recalculation on startup
|
||||
if (fSerializeChainTrust)
|
||||
READWRITE(nChainTrust);
|
||||
)
|
||||
|
||||
uint256 GetBlockHash() const
|
||||
|
||||
+3
-6
@@ -214,7 +214,6 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
|
||||
}
|
||||
|
||||
// Collect transactions into block
|
||||
map<uint256, CTxIndex> mapTestPool;
|
||||
uint64_t nBlockSize = 1000;
|
||||
uint64_t nBlockTx = 0;
|
||||
int nBlockSigOps = 100;
|
||||
@@ -266,10 +265,10 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
|
||||
|
||||
// Connecting shouldn't fail due to dependency on other memory pool transactions
|
||||
// because we're already processing them in order of dependency
|
||||
map<uint256, CTxIndex> mapTestPoolTmp(mapTestPool);
|
||||
MapPrevTx mapInputs;
|
||||
MapPrevTx mapEmpty;
|
||||
bool fInvalid;
|
||||
if (!tx.FetchInputs(txdb, mapTestPoolTmp, false, true, mapInputs, fInvalid))
|
||||
if (!tx.FetchInputs(txdb, mapEmpty, false, true, mapInputs, fInvalid))
|
||||
continue;
|
||||
|
||||
int64_t nTxFees = tx.GetValueIn(mapInputs)-tx.GetValueOut();
|
||||
@@ -280,10 +279,8 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
|
||||
if (nBlockSigOps + nTxSigOps >= MAX_BLOCK_SIGOPS)
|
||||
continue;
|
||||
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, mapTestPoolTmp, CDiskTxPos(1,1,1), pindexPrev, false, true))
|
||||
if (!tx.ConnectInputs(txdb, mapInputs, pindexPrev, false, true))
|
||||
continue;
|
||||
mapTestPoolTmp[tx.GetHash()] = CTxIndex(CDiskTxPos(1,1,1), tx.vout.size());
|
||||
swap(mapTestPool, mapTestPoolTmp);
|
||||
|
||||
// Added
|
||||
pblock->vtx.push_back(tx);
|
||||
|
||||
+16
-24
@@ -1349,40 +1349,29 @@ void ThreadOnionSeed(void* parg)
|
||||
|
||||
|
||||
|
||||
// Load hardcoded .onion seeds (if any)
|
||||
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
|
||||
|
||||
|
||||
int found = 0;
|
||||
|
||||
printf("Loading addresses from .onion seeds\n");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != NULL; seed_idx++) {
|
||||
CNetAddr parsed;
|
||||
if (
|
||||
!parsed.SetSpecial(
|
||||
strOnionSeed[seed_idx][0]
|
||||
)
|
||||
) {
|
||||
if (!parsed.SetSpecial(strOnionSeed[seed_idx][0]))
|
||||
throw runtime_error("ThreadOnionSeed() : invalid .onion seed");
|
||||
}
|
||||
|
||||
int nOneDay = 24*3600;
|
||||
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
|
||||
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old
|
||||
|
||||
found++;
|
||||
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay);
|
||||
addrman.Add(addr, parsed);
|
||||
|
||||
|
||||
|
||||
|
||||
found++;
|
||||
}
|
||||
|
||||
printf("%d addresses found from .onion seeds\n", found);
|
||||
printf("%d addresses from hardcoded .onion seeds\n", found);
|
||||
|
||||
// Also fetch dynamic seeds from HTTP seed list
|
||||
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
|
||||
printf("ThreadOnionSeed: seeding complete\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -2210,8 +2199,11 @@ void StartNode(void* parg)
|
||||
if (fUseUPnP)
|
||||
MapPort();
|
||||
|
||||
// HTTP seed list fetch (replaces DNS seeds)
|
||||
if (GetBoolArg("-noseedurl", false))
|
||||
// HTTP seed list fetch — only as a standalone thread if onion seeding is disabled,
|
||||
// since ThreadOnionSeed already calls ThreadHTTPSeedFetch2 internally.
|
||||
if (GetBoolArg("-onionseed", true))
|
||||
printf("HTTP seed fetch handled by onion seed thread\n");
|
||||
else if (GetBoolArg("-noseedurl", false))
|
||||
printf("HTTP seed fetch disabled\n");
|
||||
else if (!NewThread(ThreadHTTPSeedFetch, NULL))
|
||||
printf("Error: NewThread(ThreadHTTPSeedFetch) failed\n");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#include "net_bootstrap.h"
|
||||
#include "main.h"
|
||||
#include "net.h"
|
||||
#include "util.h"
|
||||
|
||||
@@ -32,11 +33,10 @@ namespace NetBootstrap {
|
||||
// Bootstrapped if at least 3 connections
|
||||
health.isBootstrapped = (health.connectedPeers >= 3);
|
||||
|
||||
// Sync status
|
||||
extern int64_t nTimeBestReceived;
|
||||
health.isSyncing = (nTimeBestReceived > 0 &&
|
||||
GetTime() - nTimeBestReceived < 3600);
|
||||
health.lastBlockTime = nTimeBestReceived;
|
||||
// Sync status (nTimeBestReceived declared in main.h)
|
||||
health.isSyncing = (::nTimeBestReceived > 0 &&
|
||||
GetTime() - ::nTimeBestReceived < 3600);
|
||||
health.lastBlockTime = ::nTimeBestReceived;
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
@@ -307,7 +307,6 @@ bool IntroDialog::pickDataDirectory()
|
||||
progress.setValue(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -370,20 +370,20 @@ Value signrawtransaction(const Array& params, bool fHelp)
|
||||
{
|
||||
CTransaction tempTx;
|
||||
MapPrevTx mapPrevTx;
|
||||
MapPrevTx mapEmpty;
|
||||
CTxDB txdb("r");
|
||||
map<uint256, CTxIndex> unused;
|
||||
bool fInvalid;
|
||||
|
||||
// FetchInputs aborts on failure, so we go one at a time.
|
||||
tempTx.vin.push_back(mergedTx.vin[i]);
|
||||
tempTx.FetchInputs(txdb, unused, false, false, mapPrevTx, fInvalid);
|
||||
tempTx.FetchInputs(txdb, mapEmpty, false, false, mapPrevTx, fInvalid);
|
||||
|
||||
// Copy results into mapPrevOut:
|
||||
for (const CTxIn& txin : tempTx.vin)
|
||||
{
|
||||
const uint256& prevHash = txin.prevout.hash;
|
||||
if (mapPrevTx.count(prevHash) && mapPrevTx[prevHash].second.vout.size()>txin.prevout.n)
|
||||
mapPrevOut[txin.prevout] = mapPrevTx[prevHash].second.vout[txin.prevout.n].scriptPubKey;
|
||||
MapPrevTx::const_iterator mi = mapPrevTx.find(txin.prevout);
|
||||
if (mi != mapPrevTx.end())
|
||||
mapPrevOut[txin.prevout] = mi->second.scriptPubKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+194
-84
@@ -29,6 +29,8 @@ namespace fs = boost::filesystem;
|
||||
|
||||
leveldb::DB *txdb; // global pointer for LevelDB object instance
|
||||
|
||||
bool CDiskBlockIndex::fSerializeChainTrust = false;
|
||||
|
||||
static leveldb::Options GetOptions() {
|
||||
leveldb::Options options;
|
||||
int nCacheSizeMB = GetArg("-dbcache", 2048);
|
||||
@@ -365,9 +367,22 @@ bool CTxDB::LoadBlockIndex()
|
||||
// from BDB.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check DB format version to determine serialization features.
|
||||
int nDbFormat = 1;
|
||||
ReadDbFormat(nDbFormat);
|
||||
CDiskBlockIndex::fSerializeChainTrust = (nDbFormat >= 2);
|
||||
|
||||
if (CDiskBlockIndex::fSerializeChainTrust)
|
||||
printf("LoadBlockIndex(): DB format v%d — nChainTrust persisted\n", nDbFormat);
|
||||
else
|
||||
printf("LoadBlockIndex(): DB format v%d — will recalculate nChainTrust (one-time upgrade)\n", nDbFormat);
|
||||
|
||||
// The block index is an in-memory structure that maps hashes to on-disk
|
||||
// locations where the contents of the block can be found. Here, we scan it
|
||||
// out of the DB and into mapBlockIndex.
|
||||
int64_t nPhaseStart = GetTimeMillis();
|
||||
int64_t nTotalStart = nPhaseStart;
|
||||
leveldb::Iterator *iterator = pdb->NewIterator(leveldb::ReadOptions());
|
||||
// Seek to start key.
|
||||
CDataStream ssStartKey(SER_DISK, CLIENT_VERSION);
|
||||
@@ -418,6 +433,8 @@ bool CTxDB::LoadBlockIndex()
|
||||
pindexNew->nTime = diskindex.nTime;
|
||||
pindexNew->nBits = diskindex.nBits;
|
||||
pindexNew->nNonce = diskindex.nNonce;
|
||||
// nChainTrust is populated from disk if fSerializeChainTrust, else stays 0
|
||||
pindexNew->nChainTrust = diskindex.nChainTrust;
|
||||
|
||||
// Watch for genesis block
|
||||
if (pindexGenesisBlock == NULL && blockHash == (!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet))
|
||||
@@ -428,56 +445,138 @@ bool CTxDB::LoadBlockIndex()
|
||||
return error("LoadBlockIndex() : CheckIndex failed at %d", pindexNew->nHeight);
|
||||
}
|
||||
|
||||
// triangles: build setStakeSeen
|
||||
if (pindexNew->IsProofOfStake())
|
||||
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
||||
// setStakeSeen is populated below for recent blocks only (Change D)
|
||||
|
||||
iterator->Next();
|
||||
}
|
||||
delete iterator;
|
||||
printf("STARTUP-PERF: block_index_deserialize %" PRId64 "ms blocks=%d\n", GetTimeMillis() - nPhaseStart, nBlocksLoaded);
|
||||
|
||||
if (fRequestShutdown)
|
||||
return true;
|
||||
|
||||
// Calculate nChainTrust
|
||||
vector<pair<int, CBlockIndex*> > vSortedByHeight;
|
||||
vSortedByHeight.reserve(mapBlockIndex.size());
|
||||
for (const auto& item : mapBlockIndex)
|
||||
// ---- nChainTrust: recalculate if not persisted, or verify stake modifiers ----
|
||||
nPhaseStart = GetTimeMillis();
|
||||
bool fNeedChainTrustRecalc = !CDiskBlockIndex::fSerializeChainTrust;
|
||||
|
||||
if (fNeedChainTrustRecalc)
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
vSortedByHeight.push_back(make_pair(pindex->nHeight, pindex));
|
||||
uiInterface.InitMessage(_("Calculating chain trust (one-time upgrade)..."));
|
||||
|
||||
vector<pair<int, CBlockIndex*> > vSortedByHeight;
|
||||
vSortedByHeight.reserve(mapBlockIndex.size());
|
||||
for (const auto& item : mapBlockIndex)
|
||||
vSortedByHeight.push_back(make_pair(item.second->nHeight, item.second));
|
||||
sort(vSortedByHeight.begin(), vSortedByHeight.end());
|
||||
|
||||
int nLastCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
||||
int nProgressInterval = std::max((int)vSortedByHeight.size() / 20, 1);
|
||||
int nCount = 0;
|
||||
|
||||
for (const auto& item : vSortedByHeight)
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
pindex->nChainTrust = (pindex->pprev ? pindex->pprev->nChainTrust : 0) + pindex->GetBlockTrust();
|
||||
|
||||
if (pindex->nHeight >= nLastCheckpointHeight)
|
||||
{
|
||||
pindex->nStakeModifierChecksum = GetStakeModifierChecksum(pindex);
|
||||
if (!CheckStakeModifierCheckpoints(pindex->nHeight, pindex->nStakeModifierChecksum))
|
||||
return error("CTxDB::LoadBlockIndex() : Failed stake modifier checkpoint height=%d, modifier=0x%016"PRIx64, pindex->nHeight, pindex->nStakeModifier);
|
||||
}
|
||||
|
||||
if (++nCount % nProgressInterval == 0)
|
||||
{
|
||||
std::string strMsg = strprintf(_("Calculating chain trust... (%d%%)"), nCount * 100 / vSortedByHeight.size());
|
||||
uiInterface.InitMessage(strMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade: rewrite all block index entries with nChainTrust and bump format.
|
||||
printf("LoadBlockIndex(): upgrading DB to format v3 (persisting nChainTrust + UTXO model)...\n");
|
||||
uiInterface.InitMessage(_("Upgrading block index..."));
|
||||
CDiskBlockIndex::fSerializeChainTrust = true;
|
||||
|
||||
leveldb::WriteBatch batch;
|
||||
nCount = 0;
|
||||
for (const auto& item : vSortedByHeight)
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
CDiskBlockIndex diskindex(pindex);
|
||||
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey << make_pair(string("blockindex"), *pindex->phashBlock);
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
ssValue << diskindex;
|
||||
batch.Put(ssKey.str(), ssValue.str());
|
||||
|
||||
// Flush in chunks to limit memory usage
|
||||
if (++nCount % 100000 == 0)
|
||||
{
|
||||
pdb->Write(leveldb::WriteOptions(), &batch);
|
||||
batch.Clear();
|
||||
printf("LoadBlockIndex(): upgraded %d / %d block index entries\n", nCount, (int)vSortedByHeight.size());
|
||||
}
|
||||
}
|
||||
// Write remaining entries + format version
|
||||
CDataStream ssFmtKey(SER_DISK, CLIENT_VERSION);
|
||||
ssFmtKey << string("dbformat");
|
||||
CDataStream ssFmtValue(SER_DISK, CLIENT_VERSION);
|
||||
ssFmtValue << (int)3;
|
||||
batch.Put(ssFmtKey.str(), ssFmtValue.str());
|
||||
|
||||
leveldb::Status status = pdb->Write(leveldb::WriteOptions(), &batch);
|
||||
if (!status.ok())
|
||||
return error("LoadBlockIndex(): failed to write upgraded block index: %s", status.ToString().c_str());
|
||||
|
||||
printf("LoadBlockIndex(): DB upgraded to format v3 (%d entries rewritten)\n", nCount);
|
||||
}
|
||||
sort(vSortedByHeight.begin(), vSortedByHeight.end());
|
||||
|
||||
int nLastCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
||||
int nProgressInterval = std::max((int)vSortedByHeight.size() / 20, 1);
|
||||
int nCount = 0;
|
||||
|
||||
for (const auto& item : vSortedByHeight)
|
||||
else
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
pindex->nChainTrust = (pindex->pprev ? pindex->pprev->nChainTrust : 0) + pindex->GetBlockTrust();
|
||||
|
||||
// Only compute the expensive SHA-256 stake modifier checksum for blocks
|
||||
// at or beyond the last hardcoded checkpoint. Blocks well below the
|
||||
// checkpoint have already been validated — recomputing 2M+ hashes on
|
||||
// every startup was the main cause of multi-minute load times.
|
||||
if (pindex->nHeight >= nLastCheckpointHeight)
|
||||
// nChainTrust was loaded from disk. Only need stake modifier checksums
|
||||
// for blocks above the last checkpoint (typically very few or zero).
|
||||
int nLastCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
||||
bool fNeedModifierCheck = false;
|
||||
for (const auto& item : mapBlockIndex)
|
||||
{
|
||||
pindex->nStakeModifierChecksum = GetStakeModifierChecksum(pindex);
|
||||
if (!CheckStakeModifierCheckpoints(pindex->nHeight, pindex->nStakeModifierChecksum))
|
||||
return error("CTxDB::LoadBlockIndex() : Failed stake modifier checkpoint height=%d, modifier=0x%016"PRIx64, pindex->nHeight, pindex->nStakeModifier);
|
||||
if (item.second->nHeight >= nLastCheckpointHeight)
|
||||
{
|
||||
fNeedModifierCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Report progress for UI responsiveness
|
||||
if (++nCount % nProgressInterval == 0)
|
||||
if (fNeedModifierCheck)
|
||||
{
|
||||
std::string strMsg = strprintf(_("Loading block index... (%d%%)"), nCount * 100 / vSortedByHeight.size());
|
||||
uiInterface.InitMessage(strMsg);
|
||||
vector<pair<int, CBlockIndex*> > vAboveCheckpoint;
|
||||
for (const auto& item : mapBlockIndex)
|
||||
if (item.second->nHeight >= nLastCheckpointHeight)
|
||||
vAboveCheckpoint.push_back(make_pair(item.second->nHeight, item.second));
|
||||
sort(vAboveCheckpoint.begin(), vAboveCheckpoint.end());
|
||||
|
||||
for (const auto& item : vAboveCheckpoint)
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
pindex->nStakeModifierChecksum = GetStakeModifierChecksum(pindex);
|
||||
if (!CheckStakeModifierCheckpoints(pindex->nHeight, pindex->nStakeModifierChecksum))
|
||||
return error("CTxDB::LoadBlockIndex() : Failed stake modifier checkpoint height=%d, modifier=0x%016"PRIx64, pindex->nHeight, pindex->nStakeModifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("STARTUP-PERF: chain_trust_and_modifiers %" PRId64 "ms\n", GetTimeMillis() - nPhaseStart);
|
||||
|
||||
// Bump dbformat to 3 if needed (databases that already had v2 nChainTrust upgrade).
|
||||
// UTXO entries are written by ConnectBlock during normal sync. For databases upgrading
|
||||
// from older versions, FetchInputs has a lazy fallback to the old CTxIndex path.
|
||||
if (nDbFormat < 3)
|
||||
{
|
||||
WriteDbFormat(3);
|
||||
printf("LoadBlockIndex(): bumped dbformat to v3 (UTXO model with lazy fallback)\n");
|
||||
}
|
||||
|
||||
// Load hashBestChain pointer to end of best chain
|
||||
nPhaseStart = GetTimeMillis();
|
||||
if (!ReadHashBestChain(hashBestChain))
|
||||
{
|
||||
if (pindexGenesisBlock == NULL)
|
||||
@@ -490,6 +589,26 @@ bool CTxDB::LoadBlockIndex()
|
||||
nBestHeight = pindexBest->nHeight;
|
||||
nBestChainTrust = pindexBest->nChainTrust;
|
||||
|
||||
printf("STARTUP-PERF: best_chain %" PRId64 "ms\n", GetTimeMillis() - nPhaseStart);
|
||||
|
||||
// ---- setStakeSeen: only populate for recent blocks (DoS protection) ----
|
||||
nPhaseStart = GetTimeMillis();
|
||||
{
|
||||
int nStakeSeenDepth = 500;
|
||||
CBlockIndex* pindex = pindexBest;
|
||||
int nLoaded = 0;
|
||||
while (pindex && nLoaded < nStakeSeenDepth)
|
||||
{
|
||||
if (pindex->IsProofOfStake())
|
||||
setStakeSeen.insert(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
pindex = pindex->pprev;
|
||||
nLoaded++;
|
||||
}
|
||||
printf("LoadBlockIndex(): populated setStakeSeen with %d entries (last %d blocks)\n",
|
||||
(int)setStakeSeen.size(), nLoaded);
|
||||
}
|
||||
printf("STARTUP-PERF: stake_seen %" PRId64 "ms\n", GetTimeMillis() - nPhaseStart);
|
||||
|
||||
printf("LoadBlockIndex(): hashBestChain=%s height=%d trust=%s date=%s\n",
|
||||
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight, CBigNum(nBestChainTrust).ToString().c_str(),
|
||||
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str());
|
||||
@@ -512,6 +631,7 @@ bool CTxDB::LoadBlockIndex()
|
||||
nBestInvalidTrust = bnBestInvalidTrust.getuint256();
|
||||
|
||||
// Verify blocks in the best chain
|
||||
nPhaseStart = GetTimeMillis();
|
||||
int nCheckLevel = GetArg("-checklevel", 1);
|
||||
int nCheckDepth = GetArg( "-checkblocks", 50);
|
||||
if (nCheckDepth == 0)
|
||||
@@ -563,66 +683,20 @@ bool CTxDB::LoadBlockIndex()
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
}
|
||||
// check level 4: check whether spent txouts were spent within the main chain
|
||||
unsigned int nOutput = 0;
|
||||
if (nCheckLevel>3)
|
||||
// check level 4: verify spent inputs were removed from UTXO set
|
||||
if (nCheckLevel>3 && !tx.IsCoinBase())
|
||||
{
|
||||
for (const CDiskTxPos &txpos : txindex.vSpent)
|
||||
for (const CTxIn &txin : tx.vin)
|
||||
{
|
||||
if (!txpos.IsNull())
|
||||
if (HaveUtxo(txin.prevout.hash, txin.prevout.n))
|
||||
{
|
||||
pair<unsigned int, unsigned int> posFind = make_pair(txpos.nFile, txpos.nBlockPos);
|
||||
if (!mapBlockPos.count(posFind))
|
||||
{
|
||||
printf("LoadBlockIndex(): *** found bad spend at %d, hashBlock=%s, hashTx=%s\n", pindex->nHeight, pindex->GetBlockHash().ToString().c_str(), hashTx.ToString().c_str());
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
// check level 6: check whether spent txouts were spent by a valid transaction that consume them
|
||||
if (nCheckLevel>5)
|
||||
{
|
||||
CTransaction txSpend;
|
||||
if (!txSpend.ReadFromDisk(txpos))
|
||||
{
|
||||
printf("LoadBlockIndex(): *** cannot read spending transaction of %s:%i from disk\n", hashTx.ToString().c_str(), nOutput);
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
else if (!txSpend.CheckTransaction())
|
||||
{
|
||||
printf("LoadBlockIndex(): *** spending transaction of %s:%i is invalid\n", hashTx.ToString().c_str(), nOutput);
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool fFound = false;
|
||||
for (const CTxIn &txin : txSpend.vin)
|
||||
if (txin.prevout.hash == hashTx && txin.prevout.n == nOutput)
|
||||
fFound = true;
|
||||
if (!fFound)
|
||||
{
|
||||
printf("LoadBlockIndex(): *** spending transaction of %s:%i does not spend it\n", hashTx.ToString().c_str(), nOutput);
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("LoadBlockIndex(): *** spent input still in UTXO set: %s:%i in %s\n",
|
||||
txin.prevout.hash.ToString().c_str(), txin.prevout.n, hashTx.ToString().c_str());
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
nOutput++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check level 5: check whether all prevouts are marked spent
|
||||
if (nCheckLevel>4)
|
||||
{
|
||||
for (const CTxIn &txin : tx.vin)
|
||||
{
|
||||
CTxIndex txindex;
|
||||
if (ReadTxIndex(txin.prevout.hash, txindex))
|
||||
if (txindex.vSpent.size()-1 < txin.prevout.n || txindex.vSpent[txin.prevout.n].IsNull())
|
||||
{
|
||||
printf("LoadBlockIndex(): *** found unspent prevout %s:%i in %s\n", txin.prevout.hash.ToString().c_str(), txin.prevout.n, hashTx.ToString().c_str());
|
||||
pindexFork = pindex->pprev;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -636,6 +710,8 @@ bool CTxDB::LoadBlockIndex()
|
||||
CTxDB txdb;
|
||||
block.SetBestChain(txdb, pindexFork);
|
||||
}
|
||||
printf("STARTUP-PERF: verify_blocks %" PRId64 "ms depth=%d level=%d\n", GetTimeMillis() - nPhaseStart, nCheckDepth, nCheckLevel);
|
||||
printf("STARTUP-PERF: load_block_index_total %" PRId64 "ms\n", GetTimeMillis() - nTotalStart);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -750,3 +826,37 @@ bool CTxDB::GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeigh
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- UTXO database methods ----------
|
||||
|
||||
bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
|
||||
{
|
||||
entry.SetNull();
|
||||
return Read(make_pair(string("u"), make_pair(hash, n)), entry);
|
||||
}
|
||||
|
||||
bool CTxDB::WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry)
|
||||
{
|
||||
return Write(make_pair(string("u"), make_pair(hash, n)), entry);
|
||||
}
|
||||
|
||||
bool CTxDB::EraseUtxo(const uint256& hash, unsigned int n)
|
||||
{
|
||||
return Erase(make_pair(string("u"), make_pair(hash, n)));
|
||||
}
|
||||
|
||||
bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
|
||||
{
|
||||
if (Exists(make_pair(string("u"), make_pair(hash, n))))
|
||||
return true;
|
||||
|
||||
// Lazy fallback: check old CTxIndex vSpent for databases upgrading from pre-UTXO format
|
||||
CTxIndex txindex;
|
||||
if (ReadTxIndex(hash, txindex))
|
||||
{
|
||||
if (n < txindex.vSpent.size() && txindex.vSpent[n].IsNull())
|
||||
return true; // vSpent[n] is null = output NOT spent = UTXO exists
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,17 @@ public:
|
||||
return Write(std::string("version"), nVersion);
|
||||
}
|
||||
|
||||
bool ReadDbFormat(int& nDbFormat)
|
||||
{
|
||||
nDbFormat = 1;
|
||||
return Read(std::string("dbformat"), nDbFormat);
|
||||
}
|
||||
|
||||
bool WriteDbFormat(int nDbFormat)
|
||||
{
|
||||
return Write(std::string("dbformat"), nDbFormat);
|
||||
}
|
||||
|
||||
bool ReadTxIndex(uint256 hash, CTxIndex& txindex);
|
||||
bool UpdateTxIndex(uint256 hash, const CTxIndex& txindex);
|
||||
bool AddTxIndex(const CTransaction& tx, const CDiskTxPos& pos, int nHeight);
|
||||
@@ -220,6 +231,12 @@ public:
|
||||
bool GetAddressUtxos(int nType, const uint160& hashBytes, std::vector<std::pair<COutPoint, std::pair<int64_t, int> > >& vUtxos);
|
||||
bool GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeight, int nEndHeight, std::vector<uint256>& vTxIds);
|
||||
|
||||
// UTXO database methods
|
||||
bool ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry);
|
||||
bool WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry);
|
||||
bool EraseUtxo(const uint256& hash, unsigned int n);
|
||||
bool HaveUtxo(const uint256& hash, unsigned int n);
|
||||
|
||||
private:
|
||||
bool LoadBlockIndexGuts();
|
||||
};
|
||||
|
||||
+29
-48
@@ -1103,43 +1103,27 @@ bool CWallet::ScanForWalletTransactionsFromIndex(CBlockIndex* pindexStart, bool
|
||||
wtx = (*mi).second;
|
||||
}
|
||||
|
||||
CTxIndex txindex;
|
||||
if (!txdb.ReadTxIndex(hashTx, txindex))
|
||||
// Check UTXO existence to update spent status.
|
||||
// Spending transactions are discovered through block scanning.
|
||||
if (!txdb.ContainsTx(hashTx))
|
||||
continue;
|
||||
if (txindex.vSpent.size() != wtx.vout.size())
|
||||
{
|
||||
printf("ERROR: ScanForWalletTransactionsFromIndex() : txindex.vSpent.size() %"PRIszu" != wtx.vout.size() %"PRIszu" for %s\n",
|
||||
txindex.vSpent.size(), wtx.vout.size(), hashTx.ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < txindex.vSpent.size(); i++)
|
||||
for (unsigned int i = 0; i < wtx.vout.size(); i++)
|
||||
{
|
||||
if (txindex.vSpent[i].IsNull() || !IsMine(wtx.vout[i]))
|
||||
if (!IsMine(wtx.vout[i]))
|
||||
continue;
|
||||
|
||||
CTransaction txSpend;
|
||||
CTxIndex txindexSpend;
|
||||
int nSpendHeight = 0;
|
||||
if (!ReadIndexedWalletTransaction(txdb, txindex.vSpent[i], txSpend, txindexSpend, nSpendHeight))
|
||||
return false;
|
||||
|
||||
bool fSpendExists = false;
|
||||
// If UTXO doesn't exist, the output was spent
|
||||
if (!txdb.HaveUtxo(hashTx, i))
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
fSpendExists = mapWallet.count(txSpend.GetHash()) > 0;
|
||||
if (!wtx.IsSpent(i))
|
||||
{
|
||||
CWalletTx& wtxMutable = mapWallet[hashTx];
|
||||
wtxMutable.MarkSpent(i);
|
||||
wtxMutable.WriteToDisk();
|
||||
nFound++;
|
||||
}
|
||||
}
|
||||
if (fSpendExists && !fUpdate)
|
||||
continue;
|
||||
|
||||
CWalletTx wtxSpend(this, txSpend);
|
||||
wtxSpend.SetMerkleBranch();
|
||||
if (!AddToWallet(wtxSpend))
|
||||
return false;
|
||||
nFound++;
|
||||
|
||||
if (setQueuedTxs.insert(txSpend.GetHash()).second)
|
||||
vWorkQueue.push_back(txSpend.GetHash());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,25 +1165,20 @@ void CWallet::ReacceptWalletTransactions()
|
||||
if ((wtx.IsCoinBase() && wtx.IsSpent(0)) || (wtx.IsCoinStake() && wtx.IsSpent(1)))
|
||||
continue;
|
||||
|
||||
CTxIndex txindex;
|
||||
uint256 hashTx = wtx.GetHash();
|
||||
bool fUpdated = false;
|
||||
if (txdb.ReadTxIndex(wtx.GetHash(), txindex))
|
||||
// Check UTXO database for spent status of our outputs
|
||||
if (txdb.ContainsTx(hashTx))
|
||||
{
|
||||
// Update fSpent if a tx got spent somewhere else by a copy of wallet.dat
|
||||
if (txindex.vSpent.size() != wtx.vout.size())
|
||||
{
|
||||
printf("ERROR: ReacceptWalletTransactions() : txindex.vSpent.size() %"PRIszu" != wtx.vout.size() %"PRIszu"\n", txindex.vSpent.size(), wtx.vout.size());
|
||||
continue;
|
||||
}
|
||||
for (unsigned int i = 0; i < txindex.vSpent.size(); i++)
|
||||
for (unsigned int i = 0; i < wtx.vout.size(); i++)
|
||||
{
|
||||
if (wtx.IsSpent(i))
|
||||
continue;
|
||||
if (!txindex.vSpent[i].IsNull() && IsMine(wtx.vout[i]))
|
||||
// If the UTXO doesn't exist, the output was spent
|
||||
if (!txdb.HaveUtxo(hashTx, i) && IsMine(wtx.vout[i]))
|
||||
{
|
||||
wtx.MarkSpent(i);
|
||||
fUpdated = true;
|
||||
vMissingTx.push_back(txindex.vSpent[i]);
|
||||
}
|
||||
}
|
||||
if (fUpdated)
|
||||
@@ -2670,16 +2649,17 @@ void CWallet::FixSpentCoins(int& nMismatchFound, int64_t& nBalanceInQuestion, bo
|
||||
CTxDB txdb("r");
|
||||
for (CWalletTx* pcoin : vCoins)
|
||||
{
|
||||
// Find the corresponding transaction index
|
||||
CTxIndex txindex;
|
||||
if (!txdb.ReadTxIndex(pcoin->GetHash(), txindex))
|
||||
uint256 hashTx = pcoin->GetHash();
|
||||
if (!txdb.ContainsTx(hashTx))
|
||||
continue;
|
||||
for (unsigned int n=0; n < pcoin->vout.size(); n++)
|
||||
{
|
||||
if (IsMine(pcoin->vout[n]) && pcoin->IsSpent(n) && (txindex.vSpent.size() <= n || txindex.vSpent[n].IsNull()))
|
||||
bool fUtxoExists = txdb.HaveUtxo(hashTx, n);
|
||||
// Wallet says spent but UTXO exists (meaning it's NOT spent) — lost coin
|
||||
if (IsMine(pcoin->vout[n]) && pcoin->IsSpent(n) && fUtxoExists)
|
||||
{
|
||||
printf("FixSpentCoins found lost coin %s TRI %s[%d], %s\n",
|
||||
FormatMoney(pcoin->vout[n].nValue).c_str(), pcoin->GetHash().ToString().c_str(), n, fCheckOnly? "repair not attempted" : "repairing");
|
||||
FormatMoney(pcoin->vout[n].nValue).c_str(), hashTx.ToString().c_str(), n, fCheckOnly? "repair not attempted" : "repairing");
|
||||
nMismatchFound++;
|
||||
nBalanceInQuestion += pcoin->vout[n].nValue;
|
||||
if (!fCheckOnly)
|
||||
@@ -2688,10 +2668,11 @@ void CWallet::FixSpentCoins(int& nMismatchFound, int64_t& nBalanceInQuestion, bo
|
||||
pcoin->WriteToDisk();
|
||||
}
|
||||
}
|
||||
else if (IsMine(pcoin->vout[n]) && !pcoin->IsSpent(n) && (txindex.vSpent.size() > n && !txindex.vSpent[n].IsNull()))
|
||||
// Wallet says unspent but UTXO doesn't exist (meaning it IS spent) — phantom coin
|
||||
else if (IsMine(pcoin->vout[n]) && !pcoin->IsSpent(n) && !fUtxoExists)
|
||||
{
|
||||
printf("FixSpentCoins found spent coin %s TRI %s[%d], %s\n",
|
||||
FormatMoney(pcoin->vout[n].nValue).c_str(), pcoin->GetHash().ToString().c_str(), n, fCheckOnly? "repair not attempted" : "repairing");
|
||||
FormatMoney(pcoin->vout[n].nValue).c_str(), hashTx.ToString().c_str(), n, fCheckOnly? "repair not attempted" : "repairing");
|
||||
nMismatchFound++;
|
||||
nBalanceInQuestion += pcoin->vout[n].nValue;
|
||||
if (!fCheckOnly)
|
||||
|
||||
Reference in New Issue
Block a user