Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f3fcb97
feat: migrate TimestampIndex from synchronous to async operation
UdjinM6 Jan 19, 2026
33d2fc5
test: update timestampindex functional test for async behavior
UdjinM6 Jan 19, 2026
464cdf8
feat: migrate SpentIndex from synchronous to async operation
UdjinM6 Jan 19, 2026
3ea9394
test: update feature_spentindex for async behavior
UdjinM6 Jan 19, 2026
18276a6
feat: migrate AddressIndex from synchronous to async operation
UdjinM6 Jan 19, 2026
8ad06be
test: update feature_addressindex for async behavior
UdjinM6 Jan 19, 2026
fa7be34
feat: add CompactRange template method to CDBWrapper
UdjinM6 Jan 19, 2026
fa60ccb
feat: migrate old synchronous index data to new async index databases
UdjinM6 Jan 19, 2026
6e5f7ab
fix: add missing include in `src/zmq/zmqpublishnotifier.cpp`
UdjinM6 Jan 19, 2026
5fd7033
docs: add release notes
UdjinM6 Jan 19, 2026
fd1de98
refactor: address code review feedback
UdjinM6 Jan 19, 2026
5be629b
refactor: apply clang-format
UdjinM6 Jan 19, 2026
e84b1fa
feat: add async indexes to getindexinfo RPC
UdjinM6 Jan 19, 2026
98c3c1b
fix: check index sync status before querying and show sync progress
UdjinM6 Jan 19, 2026
331335e
fix: add explicit vtxundo size validation and in-class initialization…
UdjinM6 Mar 12, 2026
97dcc6d
fix: address review suggestions
UdjinM6 Mar 27, 2026
8f3f62b
fix: add vtxundo size validation in SpentIndex::WriteBlock
UdjinM6 Mar 31, 2026
5796588
fix: reverse transaction processing order in AddressIndex::Rewind
UdjinM6 Mar 31, 2026
386acdf
fix: add undo data size validation in AddressIndex::Rewind
UdjinM6 Mar 31, 2026
1ca7374
fix: harden BlockDisconnected sibling check and getaddressbalance hei…
UdjinM6 Mar 31, 2026
c41584e
refactor: move BlockUntilSyncedToCurrentChain from index queries to R…
UdjinM6 Apr 1, 2026
0695e97
refactor: move BlockDisconnected handling to BaseIndex
UdjinM6 Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ BITCOIN_CORE_H = \
active/quorums.h \
addrdb.h \
addressindex.h \
spentindex.h \
addrman.h \
addrman_impl.h \
attributes.h \
Expand Down Expand Up @@ -253,6 +252,8 @@ BITCOIN_CORE_H = \
index/blockfilterindex.h \
index/coinstatsindex.h \
index/disktxpos.h \
index/spentindex.h \
index/spentindex_types.h \
index/timestampindex.h \
index/timestampindex_types.h \
index/txindex.h \
Expand Down Expand Up @@ -539,6 +540,7 @@ libbitcoin_node_a_SOURCES = \
index/base.cpp \
index/blockfilterindex.cpp \
index/coinstatsindex.cpp \
index/spentindex.cpp \
index/timestampindex.cpp \
index/txindex.cpp \
init.cpp \
Expand Down
82 changes: 80 additions & 2 deletions src/addressindex.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
#define BITCOIN_ADDRESSINDEX_H

#include <consensus/amount.h>
#include <script/script.h>
#include <serialize.h>
#include <uint256.h>
#include <util/std23.h>

#include <chrono>
#include <tuple>

class CScript;
struct CAddressIndexKey;
struct CMempoolAddressDelta;
struct CMempoolAddressDeltaKey;
Expand All @@ -32,6 +31,10 @@ template<> struct is_serializable_enum<AddressType> : std::true_type {};
using CAddressIndexEntry = std::pair<CAddressIndexKey, CAmount>;
using CMempoolAddressDeltaEntry = std::pair<CMempoolAddressDeltaKey, CMempoolAddressDelta>;

struct CAddressUnspentKey;
struct CAddressUnspentValue;
using CAddressUnspentIndexEntry = std::pair<CAddressUnspentKey, CAddressUnspentValue>;

struct CMempoolAddressDelta
{
public:
Expand Down Expand Up @@ -220,6 +223,81 @@ struct CAddressIndexIteratorHeightKey {
}
};

struct CAddressUnspentKey {
public:
AddressType m_address_type{AddressType::UNKNOWN};
uint160 m_address_bytes;
uint256 m_tx_hash;
uint32_t m_tx_index{0};

public:
CAddressUnspentKey() {
SetNull();
}

CAddressUnspentKey(AddressType address_type, uint160 address_bytes, uint256 tx_hash, uint32_t tx_index) :
m_address_type{address_type}, m_address_bytes{address_bytes}, m_tx_hash{tx_hash}, m_tx_index{tx_index} {};

void SetNull() {
m_address_type = AddressType::UNKNOWN;
m_address_bytes.SetNull();
m_tx_hash.SetNull();
m_tx_index = 0;
}

public:
size_t GetSerializeSize(int nType, int nVersion) const {
return 57;
}

template<typename Stream>
void Serialize(Stream& s) const {
ser_writedata8(s, std23::to_underlying(m_address_type));
m_address_bytes.Serialize(s);
m_tx_hash.Serialize(s);
ser_writedata32(s, m_tx_index);
}

template<typename Stream>
void Unserialize(Stream& s) {
m_address_type = static_cast<AddressType>(ser_readdata8(s));
m_address_bytes.Unserialize(s);
m_tx_hash.Unserialize(s);
m_tx_index = ser_readdata32(s);
}
};

struct CAddressUnspentValue {
public:
CAmount m_amount{-1};
CScript m_tx_script;
int32_t m_block_height;

public:
CAddressUnspentValue() {
SetNull();
}

CAddressUnspentValue(CAmount amount, CScript tx_script, int32_t block_height) :
m_amount{amount}, m_tx_script{tx_script}, m_block_height{block_height} {};

void SetNull() {
m_amount = -1;
m_tx_script.clear();
m_block_height = 0;
}

bool IsNull() const {
return (m_amount == -1);
}

public:
SERIALIZE_METHODS(CAddressUnspentValue, obj)
{
READWRITE(obj.m_amount, obj.m_tx_script, obj.m_block_height);
}
};

bool AddressBytesFromScript(const CScript& script, AddressType& address_type, uint160& address_bytes);

#endif // BITCOIN_ADDRESSINDEX_H
2 changes: 1 addition & 1 deletion src/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
#include <util/system.h>

#include <addressindex.h>
#include <spentindex.h>
#include <index/spentindex.h>

#include <evo/assetlocktx.h>
#include <evo/cbtx.h>
Expand Down
171 changes: 171 additions & 0 deletions src/index/spentindex.cpp
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.
Comment thread
UdjinM6 marked this conversation as resolved.

#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
Comment thread
UdjinM6 marked this conversation as resolved.
const CTxUndo& txundo = blockundo.vtxundo[i];
const uint256 txhash = tx->GetHash();
Comment thread
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/index/spentindex.cpp`:
- [SUGGESTION] lines 96-98: AddressBytesFromScript return value ignored
  AddressIndex checks the return value and skips unrecognized scripts. SpentIndex does not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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);
}
82 changes: 82 additions & 0 deletions src/index/spentindex.h
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.
Comment thread
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
Loading