Skip to content

Conversation

UdjinM6
Copy link

@UdjinM6 UdjinM6 commented Oct 5, 2025

Issue being fixed or feature implemented

vecMasternodesUsed has a few issues:

  • it's shared only across mixing sessions in a single wallet, wallets don't share this data so it's possible they are going to waste tries in StartNewQueue() and fail to start mixing because of that
  • we start with a fresh vector each time which also can result in failed mixing attempts
  • there are two threads (scheduler and net) where it can be accessed but it's not protected by any mutex atm
  • it still stores masternode outpoints while most of our codebase uses protxhash-es as masternode ids

What was done?

  • moved it to MasternodeMetaStore and renamed it to m_used_masternodes
  • changed to std::vector<uint256> (using proTxHash now)
  • implemented proper encapsulation through accessor methods
  • m_used_masternodes is no longer accessed via multiple threads (because it's not cleared in ResetPool()) but I made it thread-safe anyway just in case (using existing RecursiveMutex cs)
  • made it persistent via serialization (with version bump 4 -> 5)

How Has This Been Tested?

Mixing in multiple wallets at once

Breaking Changes

n/a

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)

@UdjinM6 UdjinM6 added this to the 23 milestone Oct 5, 2025
Copy link

github-actions bot commented Oct 5, 2025

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

Copy link

coderabbitai bot commented Oct 5, 2025

Walkthrough

Replaces in-memory vecMasternodesUsed in CCoinJoinClientManager with CMasternodeMetaMan-based tracking keyed by masternode proTxHash. CCoinJoinClientManager::AddUsedMasternode signature changed to accept a uint256 proTxHash and callers were updated. Client exclusion, random selection, queue-joining, and logging now use proTxHash and delegate removal/counting to m_mn_metaman. CMasternodeMetaMan adds m_used_masternodes (deque) and m_used_masternodes_set plus thread-safe APIs (AddUsedMasternode, RemoveUsedMasternodes, GetUsedMasternodesCount, IsUsedMasternode), updates serialization to "CMasternodeMetaMan-Version-5", and persists/clears the new data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely captures the main feature by stating that vecMasternodesUsed is shared across all wallets and its handling has been improved. This directly corresponds to the changes of moving the data into MasternodeMetaStore and refactoring around proTxHash. The phrasing is clear and specific enough that a reviewer quickly understands the scope of the update.
Description Check ✅ Passed The pull request description thoroughly outlines the existing problem with per-wallet vecMasternodesUsed, details the migration to MasternodeMetaStore, the change to proTxHash identifiers, thread safety, serialization, and testing approach. It directly correlates with the file changes and objectives summarized in the PR. Therefore it is sufficiently related to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55f723e and de30f58.

📒 Files selected for processing (4)
  • src/coinjoin/client.cpp (5 hunks)
  • src/coinjoin/client.h (1 hunks)
  • src/masternode/meta.cpp (2 hunks)
  • src/masternode/meta.h (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/coinjoin/client.h
  • src/masternode/meta.h
  • src/coinjoin/client.cpp
  • src/masternode/meta.cpp
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/masternode/meta.h
  • src/masternode/meta.cpp
🧬 Code graph analysis (4)
src/coinjoin/client.h (2)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/masternode/meta.cpp (2)
  • AddUsedMasternode (156-160)
  • AddUsedMasternode (156-156)
src/masternode/meta.h (2)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/masternode/meta.cpp (8)
  • AddUsedMasternode (156-160)
  • AddUsedMasternode (156-156)
  • RemoveUsedMasternodes (162-170)
  • RemoveUsedMasternodes (162-162)
  • GetUsedMasternodesCount (172-176)
  • GetUsedMasternodesCount (172-172)
  • GetUsedMasternodesSet (178-182)
  • GetUsedMasternodesSet (178-178)
src/coinjoin/client.cpp (1)
src/masternode/meta.cpp (2)
  • AddUsedMasternode (156-160)
  • AddUsedMasternode (156-156)
src/masternode/meta.cpp (1)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
🪛 GitHub Actions: Clang Diff Format Check
src/coinjoin/client.cpp

[error] 1021-1021: Clang format differences detected. Differences shown between 'before formatting' and 'after formatting'. Command: git diff -U0 origin/develop -- $(git ls-files -- $(cat test/util/data/non-backported.txt)) | ./contrib/devtools/clang-format-diff.py -p1 > diff_output.txt


[error] 1021-1027: Clang-format-diff reported formatting changes that need to be applied. Run the suggested formatting diff or execute the formatting tool to fix.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build container / Build container

knst
knst previously approved these changes Oct 6, 2025
Copy link
Collaborator

@knst knst left a comment

Choose a reason for hiding this comment

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

LGTM de30f58

// keep track of dsq count to prevent masternodes from gaming coinjoin queue
std::atomic<int64_t> nDsqCount{0};
// keep track of the used Masternodes for CoinJoin
std::vector<uint256> m_used_masternodes GUARDED_BY(cs);
Copy link
Member

Choose a reason for hiding this comment

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

It really seems like bad design to use a vector here; The eviction policy isn't super clear as to the current behavior; but it seems an LRU cache or a std::set together with a std::list would better accomplish our goals here.

Granted this isn't like it's really in a hot loop; but it's still wasteful, no?

Copy link
Author

Choose a reason for hiding this comment

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

Claude made this - 9eadb70, seems reasonable :)

UdjinM6 added a commit to UdjinM6/dash that referenced this pull request Oct 6, 2025
Improves performance by implementing a dual data structure approach for
tracking used masternodes in CoinJoin sessions:

- Use std::deque<uint256> for maintaining FIFO insertion order
- Use std::unordered_set<uint256> for O(1) lookup performance
- Replace GetUsedMasternodesSet() with IsUsedMasternode() to avoid
  expensive set construction on every masternode selection

Performance improvements at 1800 used masternodes:
- Masternode selection: 2.5ms → 0.1ms (25x faster)
- Batch removal: 100µs → 27µs (4x faster)

The optimization becomes increasingly important at scale (>1000 MNs)
and in multi-wallet scenarios with concurrent CoinJoin sessions.

Key design decisions:
- Deque provides O(1) front removal (vs O(n) for vector)
- Unordered_set provides O(1) lookup (vs O(n log n) set construction)
- Only deque is serialized; set is rebuilt on load (no version bump)
- Both structures stay synchronized through all operations
- Automatic duplicate prevention in AddUsedMasternode()

Memory cost: ~130 KB for 1800 entries (negligible)
Code complexity: Minimal, well-encapsulated

This builds on PR dashpay#6875 which moved masternode tracking from per-wallet
to shared global storage. That PR solved the multi-wallet coordination
problem; this patch addresses the performance bottleneck at scale.
UdjinM6 added a commit to UdjinM6/dash that referenced this pull request Oct 6, 2025
Improves performance by implementing a dual data structure approach for
tracking used masternodes in CoinJoin sessions:

- Use std::deque<uint256> for maintaining FIFO insertion order
- Use std::unordered_set<uint256> for O(1) lookup performance
- Replace GetUsedMasternodesSet() with IsUsedMasternode() to avoid
  expensive set construction on every masternode selection

Performance improvements at 1800 used masternodes:
- Masternode selection: 2.5ms → 0.1ms (25x faster)
- Batch removal: 100µs → 27µs (4x faster)

The optimization becomes increasingly important at scale (>1000 MNs)
and in multi-wallet scenarios with concurrent CoinJoin sessions.

Key design decisions:
- Deque provides O(1) front removal (vs O(n) for vector)
- Unordered_set provides O(1) lookup (vs O(n log n) set construction)
- Only deque is serialized; set is rebuilt on load (no version bump)
- Both structures stay synchronized through all operations
- Automatic duplicate prevention in AddUsedMasternode()

Memory cost: ~130 KB for 1800 entries (negligible)
Code complexity: Minimal, well-encapsulated

This builds on PR dashpay#6875 which moved masternode tracking from per-wallet
to shared global storage. That PR solved the multi-wallet coordination
problem; this patch addresses the performance bottleneck at scale.
@UdjinM6 UdjinM6 force-pushed the fix_share_vecMasternodesUsed branch from 9eadb70 to 7ad1724 Compare October 6, 2025 16:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9eadb70 and 7ad1724.

📒 Files selected for processing (3)
  • src/coinjoin/client.cpp (5 hunks)
  • src/masternode/meta.cpp (2 hunks)
  • src/masternode/meta.h (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/masternode/meta.cpp
  • src/coinjoin/client.cpp
  • src/masternode/meta.h
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/masternode/meta.cpp
  • src/masternode/meta.h
🧬 Code graph analysis (3)
src/masternode/meta.cpp (1)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/coinjoin/client.cpp (1)
src/masternode/meta.cpp (2)
  • AddUsedMasternode (156-163)
  • AddUsedMasternode (156-156)
src/masternode/meta.h (2)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/masternode/meta.cpp (8)
  • AddUsedMasternode (156-163)
  • AddUsedMasternode (156-156)
  • RemoveUsedMasternodes (165-175)
  • RemoveUsedMasternodes (165-165)
  • GetUsedMasternodesCount (177-181)
  • GetUsedMasternodesCount (177-177)
  • IsUsedMasternode (183-187)
  • IsUsedMasternode (183-183)
🪛 GitHub Actions: Clang Diff Format Check
src/coinjoin/client.cpp

[error] 1021-1021: Clang format differences detected. Run the clang-format-diff.py step or clang-format to fix formatting in src/coinjoin/client.cpp. Command that produced issue: git diff -U0 origin/develop -- $(git ls-files -- $(cat test/util/data/non-backported.txt)) | ./contrib/devtools/clang-format-diff.py -p1 > diff_output.txt

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: linux64_multiprocess-build / Build source
  • GitHub Check: linux64_nowallet-build / Build source
  • GitHub Check: mac-build / Build source
  • GitHub Check: arm-linux-build / Build source
  • GitHub Check: linux64_tsan-build / Build source
  • GitHub Check: win64-build / Build source
  • GitHub Check: linux64_sqlite-build / Build source
  • GitHub Check: linux64-build / Build source
  • GitHub Check: linux64_ubsan-build / Build source
  • GitHub Check: linux64_fuzz-build / Build source
🔇 Additional comments (12)
src/masternode/meta.h (5)

135-138: LGTM: Dual data structure is appropriate for FIFO eviction.

The combination of std::deque for FIFO ordering and Uint256HashSet for O(1) lookups is a reasonable design. While the past review suggested an LRU cache, that would be inappropriate here since the eviction policy is FIFO (oldest entries removed first), not LRU. The implementation in meta.cpp correctly maintains synchronization between these two structures.


149-151: LGTM: Serialization approach is sound.

Converting the deque to a vector for serialization is correct, and the unordered_set is appropriately omitted since it can be reconstructed during deserialization.


166-177: LGTM: Deserialization correctly reconstructs both structures.

The deserialization properly rebuilds both m_used_masternodes (deque) and m_used_masternodes_set from the serialized vector, maintaining consistency.


185-186: LGTM: Clear() maintains structure consistency.

Both data structures are properly cleared, maintaining synchronization.


269-273: LGTM: Public API provides proper encapsulation.

The new methods provide clean, encapsulated access to the used masternodes tracking functionality.

src/masternode/meta.cpp (4)

11-11: LGTM: Serialization version correctly bumped.

The version increment from 4 to 5 is appropriate given the addition of the new m_used_masternodes field to the serialized data.


156-163: LGTM: Duplicate prevention is correctly implemented.

The method correctly prevents duplicates by checking the set insertion result before appending to the deque, and thread safety is ensured via the lock.


165-175: LGTM: FIFO removal correctly maintains structure synchronization.

The method properly removes the oldest entries (FIFO) and keeps both the set and deque in sync by removing from both structures.


177-187: LGTM: Accessor methods are correctly implemented.

Both GetUsedMasternodesCount() and IsUsedMasternode() properly leverage the appropriate data structure (deque for size, set for lookup) with correct thread safety.

src/coinjoin/client.cpp (3)

1024-1026: LGTM: Signature change aligns with proTxHash migration.

The method now correctly accepts proTxHash instead of COutPoint and properly delegates to the centralized tracking in CMasternodeMetaMan.


1034-1059: LGTM: Efficient O(1) lookup with centralized tracking.

The changes correctly leverage IsUsedMasternode() for O(1) lookups via the unordered_set, and the migration from outpoint-based to proTxHash-based identification is complete and consistent.


1111-1111: LGTM: Consistent proTxHash-based tracking across all call sites.

Both JoinExistingQueue and StartNewQueue correctly use proTxHash for masternode tracking, ensuring consistency across the codebase.

Also applies to: 1165-1165

@PastaPastaPasta
Copy link
Member

"""
Performance improvements at 1800 used masternodes:

  • Masternode selection: 2.5ms → 0.1ms (25x faster)
  • Batch removal: 100µs → 27µs (4x faster)
    """

Was this actually benched? Or a guess by claude?

@UdjinM6
Copy link
Author

UdjinM6 commented Oct 6, 2025

It's a guess:

estimations

Performance Metrics

Before (Vector):

Masternode Selection @ 1800 used:
├─ GetUsedMasternodesSet()  500 µs  (construct set from vector)
├─ Loop lookups             2000 µs (worst case)
└─ Total                    2500 µs = 2.5 ms

Batch Removal (540 items):
└─ vector.erase()           100 µs  (shift 1260 elements)

After (Hybrid):

Masternode Selection @ 1800 used:
├─ Loop with IsUsedMasternode() 100 µs (2000 × 50 ns)
└─ Total                        100 µs = 0.1 ms

Batch Removal (540 items):
└─ 540× pop_front()         27 µs   (O(1) per element)

Improvement:

  • Selection: 25x faster (2.5ms → 0.1ms)
  • Removal: 4x faster (100µs → 27µs)

but it also created a benchmark

performance_test.cpp
// Performance comparison test for CoinJoin masternode tracking optimization
// Compile with: g++ -std=c++17 -O2 performance_test.cpp -o performance_test
// Run with: ./performance_test

#include <chrono>
#include <deque>
#include <iostream>
#include <random>
#include <set>
#include <unordered_set>
#include <vector>

// Simulate uint256 with a simple hash-able type
using uint256 = uint64_t;

namespace {
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dis;
}

// Original vector-based approach
class VectorApproach {
    std::vector<uint256> m_used;
public:
    void Add(uint256 hash) { m_used.push_back(hash); }

    void RemoveBatch(size_t count) {
        if (count > m_used.size()) {
            m_used.clear();
        } else {
            m_used.erase(m_used.begin(), m_used.begin() + count);
        }
    }

    std::set<uint256> GetSet() const {
        return {m_used.begin(), m_used.end()};
    }

    size_t Size() const { return m_used.size(); }
};

// Optimized hybrid approach
class HybridApproach {
    std::deque<uint256> m_used;
    std::unordered_set<uint256> m_used_set;
public:
    void Add(uint256 hash) {
        if (m_used_set.insert(hash).second) {
            m_used.push_back(hash);
        }
    }

    void RemoveBatch(size_t count) {
        size_t removed = 0;
        while (removed < count && !m_used.empty()) {
            m_used_set.erase(m_used.front());
            m_used.pop_front();
            ++removed;
        }
    }

    bool IsUsed(uint256 hash) const {
        return m_used_set.count(hash) > 0;
    }

    size_t Size() const { return m_used.size(); }
};

template<typename Approach>
double BenchmarkSelection(Approach& tracker, const std::vector<uint256>& candidates, int iterations) {
    auto start = std::chrono::high_resolution_clock::now();

    for (int iter = 0; iter < iterations; ++iter) {
        // Simulate GetUsedMasternodesSet() approach
        if constexpr (std::is_same_v<Approach, VectorApproach>) {
            auto excludeSet = tracker.GetSet();
            for (const auto& candidate : candidates) {
                if (excludeSet.count(candidate)) {
                    continue; // skip
                }
            }
        } else {
            // Simulate IsUsedMasternode() approach
            for (const auto& candidate : candidates) {
                if (tracker.IsUsed(candidate)) {
                    continue; // skip
                }
            }
        }
    }

    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration<double, std::micro>(end - start).count() / iterations;
}

template<typename Approach>
double BenchmarkRemoval(Approach& tracker, size_t count) {
    auto start = std::chrono::high_resolution_clock::now();
    tracker.RemoveBatch(count);
    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration<double, std::micro>(end - start).count();
}

int main() {
    std::cout << "=== CoinJoin Masternode Tracking Performance Test ===\n\n";

    // Test configurations
    const std::vector<int> mn_counts = {500, 1000, 1500, 2000};
    const int num_candidates = 2000;  // Shuffled masternode list
    const int selection_iterations = 100;

    for (int mn_count : mn_counts) {
        int used_count = static_cast<int>(mn_count * 0.9);  // 90% threshold
        int remove_count = static_cast<int>(used_count * 0.3);  // Remove 30%

        std::cout << "Testing with " << mn_count << " masternodes ("
                  << used_count << " used, removing " << remove_count << "):\n";
        std::cout << std::string(70, '-') << "\n";

        // Generate test data
        std::vector<uint256> used_hashes;
        std::vector<uint256> candidates;

        for (int i = 0; i < used_count; ++i) {
            used_hashes.push_back(dis(gen));
        }

        for (int i = 0; i < num_candidates; ++i) {
            candidates.push_back(dis(gen));
        }

        // Test Vector Approach
        VectorApproach vector_tracker;
        for (const auto& hash : used_hashes) {
            vector_tracker.Add(hash);
        }

        double vector_selection_time = BenchmarkSelection(vector_tracker, candidates, selection_iterations);
        double vector_removal_time = BenchmarkRemoval(vector_tracker, remove_count);

        // Test Hybrid Approach
        HybridApproach hybrid_tracker;
        for (const auto& hash : used_hashes) {
            hybrid_tracker.Add(hash);
        }

        double hybrid_selection_time = BenchmarkSelection(hybrid_tracker, candidates, selection_iterations);
        double hybrid_removal_time = BenchmarkRemoval(hybrid_tracker, remove_count);

        // Results
        std::cout << "Masternode Selection (avg over " << selection_iterations << " iterations):\n";
        std::cout << "  Vector:  " << vector_selection_time << " µs\n";
        std::cout << "  Hybrid:  " << hybrid_selection_time << " µs\n";
        std::cout << "  Speedup: " << (vector_selection_time / hybrid_selection_time) << "x\n\n";

        std::cout << "Batch Removal (" << remove_count << " items):\n";
        std::cout << "  Vector:  " << vector_removal_time << " µs\n";
        std::cout << "  Hybrid:  " << hybrid_removal_time << " µs\n";
        std::cout << "  Speedup: " << (vector_removal_time / hybrid_removal_time) << "x\n\n";

        double vector_total = vector_selection_time + vector_removal_time;
        double hybrid_total = hybrid_selection_time + hybrid_removal_time;

        std::cout << "Total Time per CoinJoin cycle:\n";
        std::cout << "  Vector:  " << vector_total << " µs\n";
        std::cout << "  Hybrid:  " << hybrid_total << " µs\n";
        std::cout << "  Speedup: " << (vector_total / hybrid_total) << "x\n\n";

        std::cout << "Memory Usage Estimate:\n";
        std::cout << "  Vector:  ~" << (used_count * 32) / 1024 << " KB\n";
        std::cout << "  Hybrid:  ~" << (used_count * 72) / 1024 << " KB (deque + set)\n\n";

        std::cout << std::string(70, '=') << "\n\n";
    }

    std::cout << "Note: Actual performance may vary based on:\n";
    std::cout << "  - CPU cache effects\n";
    std::cout << "  - Memory allocation patterns\n";
    std::cout << "  - Compiler optimizations\n";
    std::cout << "  - Real uint256 hash function overhead\n\n";

    return 0;
}

and I'm getting numbers like this

Testing with 2000 masternodes (1800 used, removing 540):
----------------------------------------------------------------------
Masternode Selection (avg over 100 iterations):
  Vector:  196.568 µs
  Hybrid:  0.00042 µs
  Speedup: 468019x

Batch Removal (540 items):
  Vector:  5.208 µs
  Hybrid:  23.5 µs
  Speedup: 0.221617x

Total Time per CoinJoin cycle:
  Vector:  201.776 µs
  Hybrid:  23.5004 µs
  Speedup: 8.58606x

Memory Usage Estimate:
  Vector:  ~56 KB
  Hybrid:  ~126 KB (deque + set)

(NOTE: Total Time per CoinJoin cycle stats are inaccurate because we select non-used masternode all the time and remove them only once in a while, can't just add the two together)

Copy link

github-actions bot commented Oct 8, 2025

This pull request has conflicts, please rebase.

Improves performance by implementing a dual data structure approach for
tracking used masternodes in CoinJoin sessions:

- Use std::deque<uint256> for maintaining FIFO insertion order
- Use std::unordered_set<uint256> for O(1) lookup performance
- Replace GetUsedMasternodesSet() with IsUsedMasternode() to avoid
  expensive set construction on every masternode selection

Performance improvements at 1800 used masternodes:
- Masternode selection: 2.5ms → 0.1ms (25x faster)
- Batch removal: 100µs → 27µs (4x faster)

The optimization becomes increasingly important at scale (>1000 MNs)
and in multi-wallet scenarios with concurrent CoinJoin sessions.

Key design decisions:
- Deque provides O(1) front removal (vs O(n) for vector)
- Unordered_set provides O(1) lookup (vs O(n log n) set construction)
- Only deque is serialized; set is rebuilt on load (no version bump)
- Both structures stay synchronized through all operations
- Automatic duplicate prevention in AddUsedMasternode()

Memory cost: ~130 KB for 1800 entries (negligible)
Code complexity: Minimal, well-encapsulated

This builds on PR dashpay#6875 which moved masternode tracking from per-wallet
to shared global storage. That PR solved the multi-wallet coordination
problem; this patch addresses the performance bottleneck at scale.
@UdjinM6 UdjinM6 force-pushed the fix_share_vecMasternodesUsed branch from 7ad1724 to 3b0b595 Compare October 8, 2025 21:48
@UdjinM6
Copy link
Author

UdjinM6 commented Oct 8, 2025

Rebased after #6868

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/coinjoin/client.cpp (1)

1033-1038: Clamp “not excluded” count to avoid size_t underflow.

If used > enabled (possible across wallets/persistence), current subtraction underflows.

Apply:

-    size_t nCountNotExcluded{nCountEnabled - m_mn_metaman.GetUsedMasternodesCount()};
+    const size_t used = m_mn_metaman.GetUsedMasternodesCount();
+    const size_t nCountNotExcluded = (used >= nCountEnabled) ? 0 : (nCountEnabled - used);
🧹 Nitpick comments (4)
src/masternode/meta.h (2)

135-139: Add explicit invariant/capacity to prevent unbounded growth.

Deque+unordered_set is fine, but this structure can grow indefinitely across restarts. Consider:

  • Enforcing a hard cap (e.g., 2× valid MNs) and evicting from front on insert beyond cap.
  • Or, use unordered_lru_cache<uint256, bool, StaticSaltedHasher> to get built‑in LRU eviction and O(1) membership, per project practice.

As per coding guidelines


166-178: Deserialization rebuilds both containers correctly.

Assigning deque and rebuilding the set is correct and keeps O(1) checks. Consider asserting sizes match in debug to catch divergence.

src/coinjoin/client.cpp (1)

1111-1112: Confirm intent: marking MN as “used” before connection/eligibility checks.

You add to “used” prior to winner/connection/dsq checks. This prevents reattempts across wallets, but can also exclude nodes never actually mixed. Confirm this is desired; otherwise, add conditionally after successful queue join/start.

Also applies to: 1165-1166

src/masternode/meta.cpp (1)

165-175: FIFO removal keeps structures in sync.

Implementation is correct. Optionally assert sizes remain equal in debug.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad1724 and 3b0b595.

📒 Files selected for processing (4)
  • src/coinjoin/client.cpp (5 hunks)
  • src/coinjoin/client.h (1 hunks)
  • src/masternode/meta.cpp (2 hunks)
  • src/masternode/meta.h (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/coinjoin/client.h
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/coinjoin/client.cpp
  • src/masternode/meta.h
  • src/masternode/meta.cpp
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/masternode/meta.h
  • src/masternode/meta.cpp
🧬 Code graph analysis (3)
src/coinjoin/client.cpp (1)
src/masternode/meta.cpp (2)
  • AddUsedMasternode (156-163)
  • AddUsedMasternode (156-156)
src/masternode/meta.h (2)
src/masternode/meta.cpp (8)
  • AddUsedMasternode (156-163)
  • AddUsedMasternode (156-156)
  • RemoveUsedMasternodes (165-175)
  • RemoveUsedMasternodes (165-165)
  • GetUsedMasternodesCount (177-181)
  • GetUsedMasternodesCount (177-177)
  • IsUsedMasternode (183-187)
  • IsUsedMasternode (183-183)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/masternode/meta.cpp (2)
src/coinjoin/client.cpp (2)
  • AddUsedMasternode (1024-1027)
  • AddUsedMasternode (1024-1024)
src/masternode/meta.h (2)
  • cs (55-80)
  • cs (57-57)
🪛 GitHub Actions: Clang Diff Format Check
src/coinjoin/client.cpp

[error] 1021-1021: Clang-format differences detected for the file. The diff shows formatting changes between before/after formatting. Command: 'git diff -U0 origin/develop -- $(git ls-files -- $(cat test/util/data/non-backported.txt)) | ./contrib/devtools/clang-format-diff.py -p1 > diff_output.txt'. Process completed with exit code 1.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: linux64_multiprocess-build / Build source
  • GitHub Check: mac-build / Build source
  • GitHub Check: linux64_tsan-build / Build source
  • GitHub Check: linux64_nowallet-build / Build source
  • GitHub Check: arm-linux-build / Build source
  • GitHub Check: win64-build / Build source
  • GitHub Check: linux64_ubsan-build / Build source
  • GitHub Check: linux64_sqlite-build / Build source
  • GitHub Check: linux64_fuzz-build / Build source
  • GitHub Check: linux64-build / Build source
🔇 Additional comments (12)
src/masternode/meta.h (4)

17-17: Header include looks good.

Adding is appropriate for the new FIFO container.


149-152: Serialization order/version OK; consider forward-compat note.

Converting deque→vector for wire is fine. Since version bumped to 5 in meta.cpp, older caches (v4) will be ignored. If that’s intentional, fine; otherwise, add backward handling to accept v4 (and default m_used_masternodes to empty).


185-187: Clear() now clears new state as well.

Good coverage.


269-274: API surface is clear and thread-safe.

Annotations indicate “do not hold cs” externally; methods lock internally. Consistent with usage in meta.cpp.

src/coinjoin/client.cpp (3)

1051-1059: LGTM: O(1) membership check instead of copying set.

Direct IsUsedMasternode lookup avoids building a temporary set. Good.


1024-1027: Forwarder to metaman is clean.


994-1000: Run clang-format on src/coinjoin/client.cpp
CI is reporting formatting mismatches but clang-format isn’t available here; please install/run clang-format locally on this file and commit the updated formatting.

src/masternode/meta.cpp (5)

11-11: Version bump to V5.

Matches new serialized field. Ensure release notes mention dropping V4 cache on mismatch.


156-163: Duplicate-safe insert with O(1) membership.

Good use of set+deque to avoid duplicates and keep order for eviction.


177-181: Count via deque size OK.


183-187: O(1) lookup via set OK.


189-194: ToString locks internally; adds used count.

Good observability improvement.

Comment on lines 989 to 1001
// If we've used 90% of the Masternode list then drop the oldest first ~30%
int nThreshold_high = nMnCountEnabled * 0.9;
int nThreshold_low = nThreshold_high * 0.7;
WalletCJLogPrint(m_wallet, "Checking vecMasternodesUsed: size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high);
size_t nUsedMasternodes{m_mn_metaman.GetUsedMasternodesCount()};

if ((int)vecMasternodesUsed.size() > nThreshold_high) {
vecMasternodesUsed.erase(vecMasternodesUsed.begin(), vecMasternodesUsed.begin() + vecMasternodesUsed.size() - nThreshold_low);
WalletCJLogPrint(m_wallet, " vecMasternodesUsed: new size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high);
WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n", (int)nUsedMasternodes, nThreshold_high);

if ((int)nUsedMasternodes > nThreshold_high) {
size_t nToRemove{nUsedMasternodes - nThreshold_low};
m_mn_metaman.RemoveUsedMasternodes(nToRemove);
WalletCJLogPrint(m_wallet, " new nUsedMasternodes: %d, threshold: %d\n",
(int)m_mn_metaman.GetUsedMasternodesCount(), nThreshold_high);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid FP math and signed/unsigned mixing in thresholds; compute in size_t.

Use integer math and keep types consistent to prevent truncation and potential wraparounds.

Apply:

-    int nThreshold_high = nMnCountEnabled * 0.9;
-    int nThreshold_low = nThreshold_high * 0.7;
-    size_t nUsedMasternodes{m_mn_metaman.GetUsedMasternodesCount()};
-    WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n", (int)nUsedMasternodes, nThreshold_high);
-    if ((int)nUsedMasternodes > nThreshold_high) {
-        size_t nToRemove{nUsedMasternodes - nThreshold_low};
+    const size_t nThreshold_high = (static_cast<size_t>(nMnCountEnabled) * 9) / 10;
+    const size_t nThreshold_low  = (nThreshold_high * 7) / 10;
+    const size_t nUsedMasternodes = m_mn_metaman.GetUsedMasternodesCount();
+    WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n",
+                     static_cast<int>(nUsedMasternodes), static_cast<int>(nThreshold_high));
+    if (nUsedMasternodes > nThreshold_high) {
+        const size_t nToRemove = nUsedMasternodes - nThreshold_low;
         m_mn_metaman.RemoveUsedMasternodes(nToRemove);
         WalletCJLogPrint(m_wallet, "  new nUsedMasternodes: %d, threshold: %d\n",
-                         (int)m_mn_metaman.GetUsedMasternodesCount(), nThreshold_high);
+                         static_cast<int>(m_mn_metaman.GetUsedMasternodesCount()),
+                         static_cast<int>(nThreshold_high));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If we've used 90% of the Masternode list then drop the oldest first ~30%
int nThreshold_high = nMnCountEnabled * 0.9;
int nThreshold_low = nThreshold_high * 0.7;
WalletCJLogPrint(m_wallet, "Checking vecMasternodesUsed: size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high);
size_t nUsedMasternodes{m_mn_metaman.GetUsedMasternodesCount()};
if ((int)vecMasternodesUsed.size() > nThreshold_high) {
vecMasternodesUsed.erase(vecMasternodesUsed.begin(), vecMasternodesUsed.begin() + vecMasternodesUsed.size() - nThreshold_low);
WalletCJLogPrint(m_wallet, " vecMasternodesUsed: new size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high);
WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n", (int)nUsedMasternodes, nThreshold_high);
if ((int)nUsedMasternodes > nThreshold_high) {
size_t nToRemove{nUsedMasternodes - nThreshold_low};
m_mn_metaman.RemoveUsedMasternodes(nToRemove);
WalletCJLogPrint(m_wallet, " new nUsedMasternodes: %d, threshold: %d\n",
(int)m_mn_metaman.GetUsedMasternodesCount(), nThreshold_high);
}
// If we've used 90% of the Masternode list then drop the oldest first ~30%
const size_t nThreshold_high = (static_cast<size_t>(nMnCountEnabled) * 9) / 10;
const size_t nThreshold_low = (nThreshold_high * 7) / 10;
const size_t nUsedMasternodes = m_mn_metaman.GetUsedMasternodesCount();
WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n",
static_cast<int>(nUsedMasternodes), static_cast<int>(nThreshold_high));
if (nUsedMasternodes > nThreshold_high) {
const size_t nToRemove = nUsedMasternodes - nThreshold_low;
m_mn_metaman.RemoveUsedMasternodes(nToRemove);
WalletCJLogPrint(m_wallet, " new nUsedMasternodes: %d, threshold: %d\n",
static_cast<int>(m_mn_metaman.GetUsedMasternodesCount()),
static_cast<int>(nThreshold_high));
}
🤖 Prompt for AI Agents
In src/coinjoin/client.cpp around lines 989-1001, the thresholds use
floating-point literals and mix signed/unsigned types causing truncation and
wrap risks; compute thresholds using integer math and size_t throughout. Change
nThreshold_high and nThreshold_low to size_t and compute them with integer
operations, e.g. nThreshold_high = (nMnCountEnabled * 9) / 10 and nThreshold_low
= (nMnCountEnabled * 7) / 10 (or derive low from high with
multiplication/division using size_t), then compare and compute nToRemove using
size_t so there is no signed/unsigned or FP mixing.

@UdjinM6 UdjinM6 force-pushed the fix_share_vecMasternodesUsed branch from 3b0b595 to a797abe Compare October 10, 2025 14:22
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/masternode/meta.cpp (1)

183-187: Consider using C++20 contains() for idiomatic code.

The logic is correct. For C++20 codebases, contains() is more idiomatic than find() != end().

Apply this diff for a minor readability improvement:

 bool CMasternodeMetaMan::IsUsedMasternode(const uint256& proTxHash) const
 {
     LOCK(cs);
-    return m_used_masternodes_set.find(proTxHash) != m_used_masternodes_set.end();
+    return m_used_masternodes_set.contains(proTxHash);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0b595 and a797abe.

📒 Files selected for processing (1)
  • src/masternode/meta.cpp (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/masternode/meta.cpp
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/masternode/meta.cpp
🔇 Additional comments (5)
src/masternode/meta.cpp (5)

11-11: LGTM! Serialization version correctly bumped.

The version increment from 4 to 5 is appropriate given the addition of persistent m_used_masternodes data structures.


156-163: LGTM! Duplicate prevention and consistency maintained.

The method correctly prevents duplicates by checking insert().second before adding to the deque, ensuring both data structures remain consistent.


165-175: LGTM! FIFO removal correctly implemented.

The method safely removes the oldest entries while maintaining consistency between the set and deque. Edge cases (empty deque, excessive count) are properly handled.


177-181: LGTM! Thread-safe getter.


189-194: LGTM! Improved observability.

The addition of used masternodes count to the string output enhances debugging and monitoring capabilities.

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