diff --git a/src/main.cpp b/src/main.cpp index 073b2ac..48d9167 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 > 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 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& 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& 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::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& 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& 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 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 mapQueuedChanges; + map 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 diff --git a/src/main.h b/src/main.h index 2091665..efdbbcf 100644 --- a/src/main.h +++ b/src/main.h @@ -431,7 +431,51 @@ enum GetMinFee_mode GMF_SEND, }; -typedef std::map > 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 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& 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& 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 diff --git a/src/miner.cpp b/src/miner.cpp index 03e9969..9565cdb 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -214,7 +214,6 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees) } // Collect transactions into block - map 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 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); diff --git a/src/net.cpp b/src/net.cpp index 04df741..3434509 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -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"); diff --git a/src/net_bootstrap.cpp b/src/net_bootstrap.cpp index d7312b1..a864e83 100644 --- a/src/net_bootstrap.cpp +++ b/src/net_bootstrap.cpp @@ -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; } diff --git a/src/qt/introdialog.cpp b/src/qt/introdialog.cpp index 81ae9d9..9b69ad1 100644 --- a/src/qt/introdialog.cpp +++ b/src/qt/introdialog.cpp @@ -307,7 +307,6 @@ bool IntroDialog::pickDataDirectory() progress.setValue(100); } } - } return true; } diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index a1390d1..f5f149b 100644 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -370,20 +370,20 @@ Value signrawtransaction(const Array& params, bool fHelp) { CTransaction tempTx; MapPrevTx mapPrevTx; + MapPrevTx mapEmpty; CTxDB txdb("r"); - map 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; } } diff --git a/src/txdb-leveldb.cpp b/src/txdb-leveldb.cpp index efdf0bf..b256d3f 100644 --- a/src/txdb-leveldb.cpp +++ b/src/txdb-leveldb.cpp @@ -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 > 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 > 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 > 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 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; +} + diff --git a/src/txdb-leveldb.h b/src/txdb-leveldb.h index 919f65c..ebaee79 100644 --- a/src/txdb-leveldb.h +++ b/src/txdb-leveldb.h @@ -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 > >& vUtxos); bool GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeight, int nEndHeight, std::vector& 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(); }; diff --git a/src/wallet.cpp b/src/wallet.cpp index 1249977..1c6c6b3 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -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)