-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat!: migrate additional indexes from synchronous to async operation #7101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
f3fcb97
33d2fc5
464cdf8
3ea9394
18276a6
8ad06be
fa7be34
fa60ccb
6e5f7ab
5fd7033
fd1de98
5be629b
e84b1fa
98c3c1b
331335e
97dcc6d
8f3f62b
5796588
386acdf
1ca7374
c41584e
0695e97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // Copyright (c) 2026 The Dash Core developers | ||
| // Distributed under the MIT software license, see the accompanying | ||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
|
||
| #include <index/spentindex.h> | ||
|
|
||
| #include <chain.h> | ||
| #include <chainparams.h> | ||
| #include <logging.h> | ||
| #include <node/blockstorage.h> | ||
| #include <primitives/block.h> | ||
| #include <primitives/transaction.h> | ||
| #include <script/script.h> | ||
| #include <undo.h> | ||
| #include <util/system.h> | ||
|
|
||
| constexpr uint8_t DB_SPENTINDEX{'p'}; | ||
|
|
||
| std::unique_ptr<SpentIndex> g_spentindex; | ||
|
|
||
| SpentIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe) : | ||
| BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / "spentindex", n_cache_size, f_memory, f_wipe) | ||
| { | ||
| } | ||
|
|
||
| bool SpentIndex::DB::WriteBatch(const std::vector<CSpentIndexEntry>& entries) | ||
| { | ||
| CDBBatch batch(*this); | ||
| for (const auto& [key, value] : entries) { | ||
| if (value.IsNull()) { | ||
| // Null value means delete entry (used during disconnect) | ||
| batch.Erase(std::make_pair(DB_SPENTINDEX, key)); | ||
| } else { | ||
| batch.Write(std::make_pair(DB_SPENTINDEX, key), value); | ||
| } | ||
| } | ||
| return CDBWrapper::WriteBatch(batch); | ||
| } | ||
|
|
||
| bool SpentIndex::DB::ReadSpentIndex(const CSpentIndexKey& key, CSpentIndexValue& value) | ||
| { | ||
| return Read(std::make_pair(DB_SPENTINDEX, key), value); | ||
| } | ||
|
|
||
| bool SpentIndex::DB::EraseSpentIndex(const std::vector<CSpentIndexKey>& keys) | ||
| { | ||
| CDBBatch batch(*this); | ||
| for (const auto& key : keys) { | ||
| batch.Erase(std::make_pair(DB_SPENTINDEX, key)); | ||
| } | ||
| return CDBWrapper::WriteBatch(batch); | ||
| } | ||
|
|
||
| SpentIndex::SpentIndex(size_t n_cache_size, bool f_memory, bool f_wipe) | ||
| : m_db(std::make_unique<SpentIndex::DB>(n_cache_size, f_memory, f_wipe)) | ||
| { | ||
| } | ||
|
|
||
| SpentIndex::~SpentIndex() = default; | ||
|
|
||
| bool SpentIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex) | ||
| { | ||
| // Skip genesis block (no inputs to index) | ||
| if (pindex->nHeight == 0) { | ||
| return true; | ||
| } | ||
|
|
||
| // Read undo data for this block to get information about spent outputs | ||
| CBlockUndo blockundo; | ||
| if (!node::UndoReadFromDisk(blockundo, pindex)) { | ||
| return error("%s: Failed to read undo data for block %s at height %d", | ||
| __func__, pindex->GetBlockHash().ToString(), pindex->nHeight); | ||
| } | ||
|
|
||
| std::vector<CSpentIndexEntry> entries; | ||
|
|
||
| // Process each non-coinbase transaction | ||
| // blockundo.vtxundo[i] corresponds to block.vtx[i+1] (coinbase is skipped in undo data) | ||
| for (size_t i = 0; i < blockundo.vtxundo.size(); i++) { | ||
| const CTransactionRef& tx = block.vtx[i + 1]; // +1 to skip coinbase | ||
|
UdjinM6 marked this conversation as resolved.
|
||
| const CTxUndo& txundo = blockundo.vtxundo[i]; | ||
| const uint256 txhash = tx->GetHash(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Process each input | ||
| if (tx->vin.size() != txundo.vprevout.size()) { | ||
| return error("%s: Undo data mismatch for tx %s", __func__, txhash.ToString()); | ||
| } | ||
|
|
||
| for (size_t j = 0; j < tx->vin.size(); j++) { | ||
| const CTxIn& input = tx->vin[j]; | ||
| const Coin& coin = txundo.vprevout[j]; | ||
| const CTxOut& prevout = coin.out; | ||
|
|
||
| AddressType address_type{AddressType::UNKNOWN}; | ||
| uint160 address_bytes; | ||
| AddressBytesFromScript(prevout.scriptPubKey, address_type, address_bytes); | ||
|
Comment on lines
+101
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: AddressBytesFromScript return value ignored AddressIndex checks the return value and skips unrecognized scripts. SpentIndex does not. source: ['claude-general'] 🤖 Fix this with AI agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a bug — preserves the same behavior as the old synchronous code on develop. In validation.cpp on develop (line ~2508), AddressBytesFromScript is called once for both address and spent index paths. The return value is only checked for the address index (if (fAddressIndex && address_type != AddressType::UNKNOWN)), while the spent index entry (line ~2521) is created unconditionally with whatever address_type and address_bytes resulted from the call. The new async code does the same thing. This is intentional: SpentIndex is keyed by (txid, output_index) — its purpose is tracking which transaction spent a given output. The address fields are auxiliary metadata. Skipping entries with unrecognized scripts would make those spends invisible to getspentinfo, breaking its core function. AddressIndex skips them because it's keyed by address — an unrecognized script can't be looked up by address anyway. |
||
|
|
||
| // Create spent index entry: spent output -> spending tx info | ||
| CSpentIndexKey key(input.prevout.hash, input.prevout.n); | ||
| CSpentIndexValue value(txhash, j, pindex->nHeight, prevout.nValue, address_type, address_bytes); | ||
|
|
||
| entries.emplace_back(key, value); | ||
| } | ||
| } | ||
|
|
||
| return m_db->WriteBatch(entries); | ||
| } | ||
|
|
||
| bool SpentIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip) | ||
| { | ||
| assert(current_tip->GetAncestor(new_tip->nHeight) == new_tip); | ||
|
|
||
| // Erase spent index entries for blocks being rewound | ||
| for (const CBlockIndex* pindex = current_tip; pindex != new_tip; pindex = pindex->pprev) { | ||
| // Skip genesis block | ||
| if (pindex->nHeight == 0) continue; | ||
|
|
||
| // Read block to get transactions | ||
| CBlock block; | ||
| if (!node::ReadBlockFromDisk(block, pindex, Params().GetConsensus())) { | ||
| return error("%s: Failed to read block %s from disk during rewind", | ||
| __func__, pindex->GetBlockHash().ToString()); | ||
| } | ||
|
|
||
| std::vector<CSpentIndexKey> keys_to_erase; | ||
|
|
||
| // Process each non-coinbase transaction | ||
| for (size_t i = 1; i < block.vtx.size(); i++) { | ||
| const CTransactionRef& tx = block.vtx[i]; | ||
|
|
||
| // Erase spent index entries for each input | ||
| for (const CTxIn& input : tx->vin) { | ||
| CSpentIndexKey key(input.prevout.hash, input.prevout.n); | ||
| keys_to_erase.push_back(key); | ||
| } | ||
| } | ||
|
|
||
| if (!keys_to_erase.empty() && !m_db->EraseSpentIndex(keys_to_erase)) { | ||
| return error("%s: Failed to erase spent index during rewind", __func__); | ||
| } | ||
| } | ||
|
|
||
| // Call base class Rewind to update the best block pointer | ||
| return BaseIndex::Rewind(current_tip, new_tip); | ||
| } | ||
|
|
||
| void SpentIndex::BlockDisconnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) | ||
| { | ||
| // When a block is disconnected (e.g., via invalidateblock), we need to rewind the index | ||
| // to remove this block's data | ||
| const CBlockIndex* best_block_index = CurrentIndex(); | ||
|
|
||
| // Only rewind if we have this block indexed | ||
| if (best_block_index && best_block_index->nHeight >= pindex->nHeight) { | ||
| if (!Rewind(best_block_index, pindex->pprev)) { | ||
| error("%s: Failed to rewind %s to previous block after disconnect", | ||
| __func__, GetName()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| BaseIndex::DB& SpentIndex::GetDB() const { return *m_db; } | ||
|
|
||
| bool SpentIndex::GetSpentInfo(CSpentIndexKey& key, CSpentIndexValue& value) const | ||
| { | ||
| if (!BlockUntilSyncedToCurrentChain()) { | ||
| return false; | ||
| } | ||
|
|
||
| return m_db->ReadSpentIndex(key, value); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // Copyright (c) 2026 The Dash Core developers | ||
| // Distributed under the MIT software license, see the accompanying | ||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| #ifndef BITCOIN_INDEX_SPENTINDEX_H | ||
| #define BITCOIN_INDEX_SPENTINDEX_H | ||
|
|
||
| #include <index/base.h> | ||
| #include <index/spentindex_types.h> | ||
|
|
||
| #include <map> | ||
|
|
||
| static constexpr bool DEFAULT_SPENTINDEX{false}; | ||
|
|
||
| struct CSpentIndexTxInfo { | ||
| std::map<CSpentIndexKey, CSpentIndexValue, CSpentIndexKeyCompare> mSpentInfo; | ||
| }; | ||
|
|
||
| /** | ||
| * SpentIndex tracks which transactions spend specific outputs. | ||
| * For each spent output, it records the spending transaction details, | ||
| * including height, amount, and address information. | ||
| * | ||
| * The index reads undo data to extract spent output information (amount, address), | ||
| * which requires that undo files are available. Therefore, this index is NOT | ||
| * compatible with pruned nodes. | ||
| * | ||
| * The index maintains a separate LevelDB database at <datadir>/indexes/spentindex/ | ||
| */ | ||
| class SpentIndex final : public BaseIndex | ||
| { | ||
| protected: | ||
| class DB; | ||
|
|
||
| private: | ||
| const std::unique_ptr<DB> m_db; | ||
|
|
||
| protected: | ||
| class DB : public BaseIndex::DB | ||
| { | ||
| public: | ||
| explicit DB(size_t n_cache_size, bool f_memory = false, bool f_wipe = false); | ||
|
|
||
| /// Write a batch of spent index entries | ||
| bool WriteBatch(const std::vector<CSpentIndexEntry>& entries); | ||
|
|
||
| /// Read spent information for a specific output | ||
| bool ReadSpentIndex(const CSpentIndexKey& key, CSpentIndexValue& value); | ||
|
|
||
| /// Erase spent index entries | ||
| bool EraseSpentIndex(const std::vector<CSpentIndexKey>& keys); | ||
| }; | ||
|
|
||
| bool WriteBlock(const CBlock& block, const CBlockIndex* pindex) override; | ||
|
|
||
| /// Custom rewind to handle spent index cleanup | ||
| bool Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip) override; | ||
|
|
||
| /// Handle block disconnections (e.g., from invalidateblock) | ||
| void BlockDisconnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) override; | ||
|
|
||
| BaseIndex::DB& GetDB() const override; | ||
| const char* GetName() const override { return "spentindex"; } | ||
|
|
||
| /// SpentIndex cannot work with pruned nodes as it requires UTXO data | ||
| bool AllowPrune() const override { return false; } | ||
|
|
||
| public: | ||
| /// Constructs a new SpentIndex. | ||
| explicit SpentIndex(size_t n_cache_size, bool f_memory = false, bool f_wipe = false); | ||
|
|
||
| /// Destructor | ||
| virtual ~SpentIndex() override; | ||
|
|
||
| /// Retrieve spent information for a specific output | ||
| bool GetSpentInfo(CSpentIndexKey& key, CSpentIndexValue& value) const; | ||
| }; | ||
|
|
||
| /// Global SpentIndex instance | ||
| extern std::unique_ptr<SpentIndex> g_spentindex; | ||
|
|
||
| #endif // BITCOIN_INDEX_SPENTINDEX_H | ||
Uh oh!
There was an error while loading. Please reload this page.