Skip to content

refactor: move address/spent/timestamp indexes into NodeContext - #7547

Open
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:refactor/move-dash-indexes-into-nodecontext
Open

refactor: move address/spent/timestamp indexes into NodeContext#7547
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:refactor/move-dash-indexes-into-nodecontext

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

AddressIndex, SpentIndex, and TimestampIndex are the last Dash-specific indexes still owned by process-global std::unique_ptrs (g_addressindex, g_spentindex, g_timestampindex). Global ownership prevents running multiple node contexts in tests, hides the mempool's dependency on index configuration behind re-declared externs, and relies on global teardown ordering. This moves them into node::NodeContext, following the same direction as #7546 (CDSNotificationInterface).

What was done?

Three commits, each self-contained:

  1. refactor: replace mempool's index-global checks with explicit enable flagsCTxMemPool::addAddressIndex/addSpentIndex only used g_addressindex/g_spentindex (via forward-declared externs in txmempool.cpp) as enable flags for the mempool's own in-memory maps. Those checks are now m_address_index_enabled/m_spent_index_enabled bools on the mempool, populated from -addressindex/-spentindex via kernel::MemPoolOptions in ApplyArgsManOptions. Since index creation is gated by exactly the same args, behavior is identical, and the extern re-declaration hack is deleted.
  2. refactor: pass SpentIndex into TxToJSON instead of reading a globalTxToJSON is a free function with no access to the request context; it now takes const SpentIndex* and callers (getrawtransaction, getrawtransactionmulti, getspecialtxes) pass it in.
  3. refactor: move address/spent/timestamp indexes into NodeContext — the three unique_ptrs become NodeContext members. Creation/Start() (init step 8), Interrupt(), and PrepareShutdown() operate on the members in the same positions and order as before (in particular, indexes are still stopped only after the second FlushBackgroundCallbacks()). RPC handlers reach them via EnsureAnyNodeContext(request.context); all user-visible error strings are unchanged. The globals are deleted.

Notes for reviewers:

  • Three new entries in test/lint/lint-circular-dependencies.py are deliberate: node/context.cpp must include the index headers so NodeContext's out-of-line destructor can delete the unique_ptr members, and index/base.cpp already includes node/context.h. This is the same module-level cycle shape as the pre-existing tolerated index/base -> node/context -> net_processing -> index/txindex -> index/base entry. These entries would collapse if/when the tree adopts upstream's node.indexes ownership for the remaining index globals (g_txindex, g_coin_stats_index, blockfilter indexes), which is a natural follow-up.
  • The indexes are declared last in NodeContext, so they are destroyed first — before anything they depend on.

How Has This Been Tested?

Built locally on macOS (autotools). feature_addressindex.py, feature_spentindex.py, feature_timestampindex.py all pass (these cover the moved query paths, the mempool overlays of getaddressmempool/getspentinfo, and getrawtransaction spent-info output). test/lint/lint-circular-dependencies.py and test/lint/lint-whitespace.py pass.

Breaking Changes

None. RPC behavior, error messages, and startup/shutdown ordering are unchanged.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

@thepastaclaw

thepastaclaw commented Aug 5, 2026

Copy link
Copy Markdown

🔍 Review in progress — actively reviewing now (commit 67a915a)
Stage: Codex precheck starting
ETA: complete ~23:00 UTC (median 20m across 30 recent reviews)
Running 9m · Last checked: 2026-08-05 22:50 UTC

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Potential PR merge conflicts

This is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order.

If this PR merges first

These open PRs will likely need a rebase:

If these PRs merge first

This PR will likely need a rebase:

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d42bd2c6-a749-464b-b65f-a51e70dc2f9a

📥 Commits

Reviewing files that changed from the base of the PR and between 43ec910 and 67a915a.

📒 Files selected for processing (17)
  • src/index/addressindex.cpp
  • src/index/addressindex.h
  • src/index/spentindex.cpp
  • src/index/spentindex.h
  • src/index/timestampindex.cpp
  • src/index/timestampindex.h
  • src/init.cpp
  • src/kernel/mempool_options.h
  • src/node/context.cpp
  • src/node/context.h
  • src/node/mempool_args.cpp
  • src/rpc/blockchain.cpp
  • src/rpc/node.cpp
  • src/rpc/rawtransaction.cpp
  • src/txmempool.cpp
  • src/txmempool.h
  • test/lint/lint-circular-dependencies.py
💤 Files with no reviewable changes (6)
  • src/index/addressindex.h
  • src/index/timestampindex.h
  • src/index/addressindex.cpp
  • src/index/spentindex.cpp
  • src/index/timestampindex.cpp
  • src/index/spentindex.h
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/init.cpp
  • src/node/mempool_args.cpp
  • test/lint/lint-circular-dependencies.py
  • src/node/context.h
  • src/kernel/mempool_options.h
  • src/rpc/rawtransaction.cpp
  • src/rpc/blockchain.cpp
  • src/rpc/node.cpp
  • src/txmempool.cpp
  • src/txmempool.h

Walkthrough

Address, spent, and timestamp indexes are now owned by NodeContext instead of global pointers. Initialization, interruption, shutdown, and RPC access use the node-owned instances. Mempool indexing uses explicit enablement options. Transaction JSON conversion receives a spent-index pointer from its callers.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RPCClient
  participant NodeContext
  participant IndexService
  RPCClient->>NodeContext: request index-backed RPC
  NodeContext->>IndexService: access configured index
  IndexService-->>NodeContext: return synchronization and lookup data
  NodeContext-->>RPCClient: return RPC response
Loading

Possibly related PRs

  • dashpay/dash#7525: Both PRs modify address, spent, and timestamp index classes, but this PR removes global ownership while that PR refactors constructors.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes moving the address, spent, and timestamp indexes into NodeContext.
Description check ✅ Passed The description directly explains the ownership refactor, related dependency changes, testing, and preserved behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@knst

knst commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Bitcoin still keep them as globals:

src/index/txindex.h:extern std::unique_ptr<TxIndex> g_txindex;

Why should we diversify codebases here?

I don't see visible benefits to do it right now. Also bitcoin still hasn't get merged bitcoin#24230 which should make proper solution so I would wait that one get merged first.

@knst

knst commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

one more thing:
index/base -> node/context -> net_processing -> index/txindex -> index/base
is not equivalient to index/base -> node/context -> index/txindex -> index/base

Because in one case node/context is a key element that making circle, and in other case it's net_processing.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

The three indexes moved here are Dash-specific — AddressIndex, SpentIndex, and TimestampIndex have no upstream counterpart, so there's no shared code to diverge from in these files; the globals themselves were our invention. The upstream-shared indexes (g_txindex, g_coin_stats_index, the blockfilter registry) are deliberately untouched, so every site a Bitcoin backport can conflict with keeps the exact global pattern upstream uses. The init/RPC hunks touched here are all Dash-only blocks (getaddressbalance, getspentinfo, getblockhashes, …). The benefits aren't speculative either — they're in the diff: txmempool.cpp no longer re-declares extern std::unique_ptr<AddressIndex> g_addressindex; just to use it as an enable flag, TxToJSON stops reading a global, and the indexes become per-context like every other Dash subsystem already is (dmnman, govman, llmq_ctx, active_ctx, and CDSNotificationInterface in #7546). They were the last Dash-owned components still living in globals, and the NodeContext doc comment upstream wrote ("More variables could be added to this struct … to eliminate use of globals and make code more modular and testable") is explicitly inviting this.

On waiting for bitcoin#24230: I don't think it's the right thing to gate on. It's been a draft since Feb 2022 (re-pushed July 2026, CI currently red, long conflict list), and more importantly it isn't an ownership PR — its commits pull sync logic out of the index classes and into node code so indexes can eventually run out-of-process. It never moves g_txindex into NodeContext, so there's nothing here that diverges from it.

On the cycles — fair distinction, agreed: the new entries put node/context directly in the cycle, where the existing one only reaches index/txindex through net_processing. But note what the edges carry. net_processing -> index/txindex is functional coupling (message handling calls g_txindex->HasTx()). The new node/context -> index/* edges carry no calls at all — context.cpp includes those headers solely so the out-of-line ~NodeContext can instantiate the unique_ptr deleters, exactly as it already does for txmempool.h, llmq/context.h, etc. The lint works at module granularity and can't tell a destructor-instantiation include from a real dependency; semantically the new cycles are weaker than the tolerated one.

And this is where 24230 actually cuts the other way: all four entries — the pre-existing txindex one included — share the same root edge, index/base -> node/context, which exists only because BaseIndex::Start() does m_chain->context()->chainman->ActiveChainstate(). The comment above that line in our own tree already says: "m_chainstate member gives indexing code access to node internals. It is removed in followup bitcoin#24230." So the allowlist entries are precisely as temporary as 24230: when it lands and we backport it, all four cycles disappear together. Ownership-in-NodeContext now plus the 24230 backport later composes cleanly; the reverse order just keeps the Dash indexes global in the meantime for no benefit.

(I did look at avoiding the entries outright — unique_ptr<BaseIndex> members still cycle index/base <-> node/context directly and force downcasts in RPC; deleter-erasure/pimpl is worse than three allowlist lines already scheduled for deletion by the thing they'd be waiting on.)


🤖 Posted autonomously by Claude on behalf of pasta.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

If the underlying worry is compile time, this cycle is the kind that has none. Compile-time cost from circular dependencies comes from header→header edges: a widely-included header transitively pulling more headers (bigger preprocessed TUs everywhere), or a low-level header depending on a high-level one (wide rebuild cascades when it changes). This PR adds no header→header edges at all: node/context.h gains only three forward declarations, so every TU that includes it preprocesses to the same thing as before, and touching node/context.h rebuilds exactly the same set of TUs as on develop.

The new cycle edges live entirely in .cpp files, where an include affects exactly one TU: node/context.cpp (+3 index headers, for the unique_ptr deleter instantiation), node/mempool_args.cpp (+2, for the DEFAULT_* constants), and rpc/blockchain.cpp (+1). rpc/node.cpp already included all three index headers before this PR. The total incremental-build delta is that editing an index header now rebuilds two additional small TUs (context.cpp, mempool_args.cpp) — and index headers are edited rarely. Nothing that includes node/context.h (init, rpc, interfaces, tests, …) is affected in any way.

That's also the general property of this cycle class: a module-level cycle closed through .cpp-side includes is invisible to the build graph's fan-out; the lint flags it because it can't distinguish include-for-deleter from include-for-use. The header-graph direction stays strictly index/* -> node/context (pre-existing, one TU: base.cpp), never the reverse.


🤖 Posted autonomously by Claude on behalf of pasta.

@knst

knst commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

If the underlying worry is compile time, this cycle is the kind that has none

no, not at all. Every newly introduced exception for circular dependencies firstly shadowing new circular dependencies that are super-set of already existing loop. Secondly, breaking them in the future is always extra efforts to do

Secondly, the main reason is diversification of codebases.

Yes, indeed, g_txindex is bitcoin + dash used but g_timestampindex and 2 others are dash specific.

Though,
they are very much related, implementing the same interface and following the same usages all over code base. What exactly does it improve? Get rid of global variable? Is there any specific case where is any tiniest benefits to out-weight of codebase diversification?

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex + Sonnet

The ownership migration and RPC/mempool plumbing are behaviorally consistent, with no stale global references or correctness failures found. One architectural concern remains: the change introduces and permanently whitelists three direct NodeContext/index dependency cycles while BaseIndex still has its explicitly temporary dependency on NodeContext. There is also one minor whitespace cleanup.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — final-verifier (Sol fallback after Sonnet verifier failure)
  • Sonnet reviewers: claude-sonnet-5 — general (completed), claude-sonnet-5 — dash-core-commit-history (failed), claude-sonnet-5 — dash-core-commit-history (completed)

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments 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/node/context.cpp`:
- [SUGGESTION] src/node/context.cpp:19-21: Resolve the temporary BaseIndex dependency before adding direct index cycles
  These concrete index includes are required to destroy the new unique_ptr members, but they combine with `BaseIndex::Start()` dereferencing `m_chain->context()` to create three new direct cycles through `node/context`. These differ from the existing temporary cycles, where the return edge to an index passes through `net_processing`, and the PR adds permanent Dash lint exceptions for them. The comment in `src/index/base.cpp` already identifies its NodeContext access as temporary pending bitcoin#24230. Resolve that dependency first, use an ownership structure that does not require these direct includes, or defer this migration instead of expanding the circular-dependency allowlist. The `<node/context.h>` include in `index/base.cpp` cannot simply be removed because line 404 requires the complete `NodeContext` definition.

Comment thread src/node/context.cpp
Comment on lines +19 to +21
#include <index/addressindex.h>
#include <index/spentindex.h>
#include <index/timestampindex.h>

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: Resolve the temporary BaseIndex dependency before adding direct index cycles

These concrete index includes are required to destroy the new unique_ptr members, but they combine with BaseIndex::Start() dereferencing m_chain->context() to create three new direct cycles through node/context. These differ from the existing temporary cycles, where the return edge to an index passes through net_processing, and the PR adds permanent Dash lint exceptions for them. The comment in src/index/base.cpp already identifies its NodeContext access as temporary pending bitcoin#24230. Resolve that dependency first, use an ownership structure that does not require these direct includes, or defer this migration instead of expanding the circular-dependency allowlist. The <node/context.h> include in index/base.cpp cannot simply be removed because line 404 requires the complete NodeContext definition.

source: ['claude']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being litigated in the PR-level thread (see the discussion with knst), so just the short version here: the new node/context -> index/* edges are destructor-instantiation includes with no calls behind them, and every one of these cycles — including the pre-existing txindex entry — depends on the single index/base -> node/context edge from BaseIndex::Start(), which is exactly what bitcoin#24230 removes. When that lands and is backported, all four allowlist entries drop out together. Deferring NodeContext ownership until then would keep the Dash indexes global for no benefit, since 24230 moves sync logic, not ownership.


🤖 Posted autonomously by Claude on behalf of pasta.

Comment thread src/txmempool.cpp
Comment on lines 33 to 38
#include <optional>
#include <ranges>

// Forward declarations for index globals and utilities
class AddressIndex;
class SpentIndex;
extern std::unique_ptr<AddressIndex> g_addressindex;
extern std::unique_ptr<SpentIndex> g_spentindex;

bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Remove the extra blank line left by the deleted declarations

Deleting the index-global declarations left two blank lines between the standard-library includes and TestLockPointValidity. Remove one of them to retain the surrounding file's normal spacing.

Suggested change
#include <optional>
#include <ranges>
// Forward declarations for index globals and utilities
class AddressIndex;
class SpentIndex;
extern std::unique_ptr<AddressIndex> g_addressindex;
extern std::unique_ptr<SpentIndex> g_spentindex;
bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp)
{
#include <cmath>
#include <memory>
#include <optional>
#include <ranges>
bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp)
{

source: ['claude']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — folded into the first commit (96db4897517) and force-pushed.


🤖 Posted autonomously by Claude on behalf of pasta.

@PastaPastaPasta
PastaPastaPasta force-pushed the refactor/move-dash-indexes-into-nodecontext branch from 5b3e9b0 to 67a915a Compare August 5, 2026 21:35
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Is there any specific case where is any tiniest benefits to out-weight of codebase diversification?

Three specific ones:

  1. Link-time coupling. txmempool.cpp referencing the g_addressindex/g_spentindex symbols means anything that links the mempool needs the index objects linked in too. We've already paid this exact cost once with an index global: 2b2d0a4 had to move CDeterministicMN::ToJson into core_write specifically "to avoid g_txindex dependency for linking". After this PR the mempool carries two bools and links standalone — that extern re-declaration hack in txmempool.cpp is deleted outright.

  2. Test isolation and teardown. These indexes hold LevelDB handles. As globals, a unit test that enabled one leaks it (and its mempool side effects) into every subsequent test in the same binary, and any path that skips PrepareShutdown hands destruction to cross-TU static-destructor ordering — the classic shutdown-crash class. As NodeContext members they're constructed and destroyed per-TestingSetup, deterministically, and always before the things they reference. That's what makes in-process unit coverage of the Dash indexes tractable at all; today they're functional-test-only.

  3. Consistency with where Dash state already lives. dmnman, govman, mn_sync, llmq_ctx, active_ctx, and now CDSNotificationInterface (refactor: move CDSNotificationInterface into NodeContext #7546) are all NodeContext-owned; the three Dash indexes were the last Dash-owned components still in globals, and their RPCs reaching them through EnsureAnyNodeContext is the same shape every other Dash RPC already uses.

On diversification: every file this touches is Dash-only (index/{address,spent,timestamp}index.*, the Dash RPCs, Dash-only init hunks), while g_txindex/g_coin_stats_index/blockfilter keep the upstream global pattern byte-for-byte — so the conflict surface for future backports is unchanged. And the fact that the Dash indexes implement the same BaseIndex interface is what makes the eventual unification cheap, not what argues for keeping them global: whenever bitcoin#24230 (or a node.indexes-style ownership PR) is merged and backported, the Dash side is already reached through node. and folds in mechanically — and that same backport deletes the index/base -> node/context edge, which removes all four allowlist entries (the pre-existing txindex one included) with zero extra effort. So "breaking them in the future" isn't extra work here; it falls out of the very PR you'd be waiting for.

On shadowing: the lint matches exact chains, so a future cycle passing through any additional module produces a different string and still fails. What the entry does tolerate is someone later adding a real call-dependency along the same edge — which is the identical exposure the existing txindex entry already has. Happy to annotate the three entries with a # remove when bitcoin#24230 is backported marker so the cleanup obligation is recorded in-tree rather than only in this thread.


🤖 Posted autonomously by Claude on behalf of pasta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants