Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
169 changes: 168 additions & 1 deletion src/coinjoin/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

#include <chain.h>
#include <chainparams.h>
#include <coins.h>
#include <core_io.h>
#include <net.h>
#include <txmempool.h>
#include <netmessagemaker.h>
#include <shutdown.h>
#include <util/check.h>
Expand All @@ -28,6 +30,7 @@
#include <wallet/coinselection.h>
#include <wallet/receive.h>
#include <wallet/spend.h>
#include <wallet/walletdb.h>

#include <memory>
#include <ranges>
Expand Down Expand Up @@ -577,6 +580,33 @@ void CCoinJoinClientSession::CompletedTransaction(PoolMessage nMessageID)
if (nMessageID == MSG_SUCCESS) {
m_clientman.UpdatedSuccessBlock();
keyHolderStorage.KeepAll();
// Our inputs are now committed to the finalized mixing transaction but the
// DSCOMPLETE message may arrive well before the (trickle-relayed) DSTX does.
// Do not release those inputs yet: keep them locked until the wallet actually
// observes a transaction spending them, otherwise another session could pick
// them up in the meantime (esp. with -coinjoinmultisession) and double-spend
// them. Anything else we locked (e.g. collateral inputs which may never be
// spent) is unlocked immediately as before.
std::set<COutPoint> setMixingInputs;
{
LOCK(cs_coinjoin);
for (const auto& entry : vecEntries) {
for (const auto& txdsin : entry.vecTxDSIn) {
setMixingInputs.emplace(txdsin.prevout);
}
}
}
std::vector<COutPoint> vecPending;
std::vector<COutPoint> vecUnlockNow;
for (const auto& outpoint : vecOutPointLocked) {
if (setMixingInputs.count(outpoint)) {
vecPending.push_back(outpoint);
} else {
vecUnlockNow.push_back(outpoint);
}
}
vecOutPointLocked = std::move(vecUnlockNow);
m_clientman.AddPendingObservation(vecPending);
WalletCJLogPrint(m_wallet, "CompletedTransaction -- success\n");
} else {
WITH_LOCK(m_wallet->cs_wallet, keyHolderStorage.ReturnAll());
Expand All @@ -592,6 +622,135 @@ void CCoinJoinClientManager::UpdatedSuccessBlock()
nCachedLastSuccessBlock = nCachedBlockHeight;
}

void CCoinJoinClientManager::AddPendingObservation(const std::vector<COutPoint>& outpoints)
{
AssertLockNotHeld(cs_pending_obs);
if (outpoints.empty()) return;

LOCK(m_wallet->cs_wallet);
LOCK(cs_pending_obs);
wallet::WalletBatch batch(m_wallet->GetDatabase());
LoadPendingObservations(batch);

const int64_t nNow{GetTime()};
bool fPersisted{true};
for (const auto& outpoint : outpoints) {
// Persist the lock so that a restart before the finalized transaction is
// observed cannot make the input available for selection again
fPersisted &= m_wallet->LockCoin(outpoint, &batch);
m_pending_obs.emplace(outpoint, nNow);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve user-persisted locks on active session inputs

When a user makes an already session-locked input persistent with lockunspent false ... true, the RPC explicitly allows the operation and writes a lockedutxo record, but this call cannot distinguish that user-owned record and adopts the outpoint as a CoinJoin pending lock. If the finalized transaction never propagates, the timeout path later calls UnlockCoin and erases the user's persistent lock, making the coin selectable contrary to the user's request. Track whether persistence already existed before adopting the input, and add a timeout test for that case.

AGENTS.md reference: AGENTS.md:L169-L171

Useful? React with 👍 / 👎.

WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- %s is locked until the finalized mixing transaction is observed\n",
__func__, outpoint.ToStringShort());
}
fPersisted &= batch.WriteCoinJoinPendingObs(m_pending_obs);
if (!fPersisted) {
// The in-memory lock still protects these inputs for as long as this process
// runs, but a restart would make them selectable again while a valid mixing
// transaction spending them may already be in flight. Nothing we can do about
// it here beyond making the failure loud - the wallet database is broken.
LogPrintf("CCoinJoinClientManager::%s -- ERROR: failed to persist locks for %d successfully mixed input(s), "
"they will not survive a restart\n",
__func__, outpoints.size());
}
}

void CCoinJoinClientManager::LoadPendingObservations(wallet::WalletBatch& batch)
{
AssertLockHeld(cs_pending_obs);

if (m_pending_obs_loaded) return;
m_pending_obs_loaded = true;
// Missing record simply means nothing was pending when we last shut down
if (!batch.ReadCoinJoinPendingObs(m_pending_obs)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not mark failed pending reads as loaded

When a wallet already has pending observations and this first lazy database read fails—due to a transient I/O error or an unreadable cj_pending_obs value—the code sets m_pending_obs_loaded before checking the result and subsequently treats the empty or partially deserialized map as authoritative. Future checks never retry, leaving the corresponding persisted lockedutxo records without an owner that can release them; a later successful AddPendingObservation can also overwrite the database record while omitting those older inputs. Only mark the state loaded after a successful read, or distinguish a genuinely absent record from a read/deserialization failure.

Useful? React with 👍 / 👎.

WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- loaded %d pending observation(s)\n", __func__,
m_pending_obs.size());
}

void CCoinJoinClientManager::CheckPendingObservations(const CTxMemPool& mempool)
{
AssertLockNotHeld(cs_pending_obs);

// nothing to do in the common case, don't touch the wallet at all
if (WITH_LOCK(cs_pending_obs, return m_pending_obs_loaded && m_pending_obs.empty())) return;

LOCK(m_wallet->cs_wallet);
LOCK(cs_pending_obs);

wallet::WalletBatch batch(m_wallet->GetDatabase());
LoadPendingObservations(batch);

const int64_t nNow{GetTime()};
const bool fSynced{m_mn_sync.IsBlockchainSynced()};
bool fChanged{false};
for (auto it = m_pending_obs.begin(); it != m_pending_obs.end();) {
const COutPoint& outpoint = it->first;
if (!m_wallet->IsLockedCoin(outpoint)) {
// The user released the lock manually (e.g. via lockunspent), respect that
it = m_pending_obs.erase(it);
fChanged = true;
continue;
}
if (m_wallet->IsSpent(outpoint)) {
// The wallet observed a transaction spending this input, it is no longer
// selectable anyway, drop the protective lock
WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- observed spend of %s, releasing lock\n", __func__,
outpoint.ToStringShort());
m_wallet->UnlockCoin(outpoint, &batch);
it = m_pending_obs.erase(it);
Comment on lines +762 to +763

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain tracking when persistent unlock fails

If the wallet database returns an I/O error while erasing lockedutxo, CWallet::UnlockCoin has already removed the in-memory lock but returns false; this result is ignored, the pending entry is erased, and the reduced pending map can still be persisted. After restart the surviving lockedutxo record recreates a permanent lock with no pending owner to release it; only erase the observation after the persistent unlock succeeds, including in the timeout branch.

Useful? React with 👍 / 👎.

fChanged = true;
continue;
}
// Only ever consult chain/mempool spentness on a synced chain: while catching up
// the spending transaction may sit in a block we have not downloaded yet, and
// releasing the input on the strength of that would defeat the whole point
if (fSynced && nNow - it->second >= PENDING_OBSERVATION_TIMEOUT_SECONDS) {
// NOTE: findCoins() reports outputs which exist in the chain UTXO set or are
// created by a mempool transaction; it does NOT know whether a mempool
// transaction spends them (CCoinsViewMemPool::GetCoin never looks at
// mapNextTx). The finalized mixing transaction sitting unprocessed in our own
// mempool is exactly the case we must not misread as "never propagated", so
// ask the mempool about spentness explicitly as well.
std::map<COutPoint, Coin> coins{{outpoint, Coin{}}};
m_wallet->chain().findCoins(coins);
if (!coins.at(outpoint).IsSpent() && !mempool.isSpent(outpoint)) {
Comment on lines +777 to +779

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Acquire chain locks before the wallet lock

CheckPendingObservations() holds m_wallet->cs_wallet from line 676 while calling chain().findCoins(). The node implementation delegates to node::FindCoins(), which acquires cs_main and the mempool lock at src/node/coin.cpp:16, establishing the prohibited order cs_wallet -> cs_main. Once an entry reaches the timeout, the periodic maintenance task can deadlock with a path using the required cs_main -> cs_wallet order. Inspect chain and mempool spentness without holding cs_wallet, then acquire the wallet lock and revalidate the pending entry before changing its lock state.

source: ['codex']

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.

Lock ordering (cs_wallet → cs_main) — FALSE POSITIVE
The finding's premise is that cs_wallet → cs_main is "the prohibited order." It isn't — it's the established one in this codebase:

wallet/spend.cpp:1170 — FundTransaction takes LOCK(wallet.cs_wallet) eight lines before wallet.chain().findCoins(coins). That's upstream Bitcoin code.
coinjoin/client.cpp:453 — the pre-existing SignFinalTransaction takes cs_wallet, then calls findCoins at line 536.
For the deadlock to be reachable there must be a cs_main → cs_wallet path. I looked for one:

No wallet function asserts or requires cs_main (grep AssertLockHeld(cs_main) / EXCLUSIVE_LOCKS_REQUIRED(cs_main across src/wallet/ returns nothing).
Every validation-interface callback the wallet consumes — TransactionAddedToMempool, BlockConnected, BlockDisconnected, UpdatedBlockTip, ChainStateFlushed, NotifyChainLock, NotifyTransactionLock — goes through ENQUEUE_AND_LOG_EVENT onto the scheduler queue. They run on the scheduler thread with no cs_main held. The wallet's proxy notably uses UpdatedBlockTip, not the synchronous variant.
Empirically: these builds are -DDEBUG_LOCKORDER, which aborts on an observed inversion. wallet_fundrawtransaction.py exercises cs_wallet → cs_main heavily alongside block connection and mempool activity, and passes with no inconsistency reported.

The suggested remedy would also introduce the bug it warns about: releasing cs_wallet mid-loop while still holding cs_pending_obs establishes cs_pending_obs → cs_wallet, inverting against AddPendingObservation, which takes them the other way. Plus a TOCTOU gap between the spentness query and the unlock decision. No change made.

The doc reference the finding leans on (developer-notes.md:598) is a generic illustration of what deadlock is — it doesn't declare a required order between these two.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved in f9c4bebAcquire chain locks before the wallet lock no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Comment on lines +778 to +779

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check chain and mempool spentness under one lock snapshot

When the timeout check races with the finalized transaction being mined, findCoins() can return the chain output while the transaction is still in the mempool, then block connection can spend the chain output and remove the transaction before the separate mempool.isSpent() call; both operands consequently report "unspent" and the lock is released before the queued wallet notification records the spend. The fresh evidence in the current revision is that the added mempool check uses a second lock acquisition rather than the same cs_main/mempool snapshot as findCoins(); query both states atomically.

AGENTS.md reference: AGENTS.md:L169-L171

Useful? React with 👍 / 👎.

Comment on lines +777 to +779

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Take an atomic chain-and-mempool spentness snapshot

findCoins() and mempool.isSpent() observe state under separate lock scopes. After findCoins() reports the chain output as unspent, block connection can spend that output and remove its spending transaction from the mempool before isSpent() runs, causing both checks to indicate that it is unspent. The wallet's queued BlockConnected callback cannot update CWallet::IsSpent() while this function holds cs_wallet, so the code can remove the pending entry and protective lock during the pre-wallet-observation window this PR is intended to close. Query chain UTXO existence and mempool mapNextTx spentness through one operation holding cs_main and the mempool lock for a coherent snapshot.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep inputs locked while an ISLock awaits its transaction

When a verified InstantSend lock reaches this node before its transaction and the transaction remains unavailable past the one-hour timeout, NetInstantSend::ProcessInstantSendLock retains that lock in pendingNoTxInstantSendLocks, but this condition sees neither a chain nor mempool spend and releases the wallet lock. The input can then enter another CoinJoin session even though the network has already committed an ISLock for the missing transaction; include pending InstantSend-lock state in the timeout decision rather than treating this case as non-propagation.

AGENTS.md reference: AGENTS.md:L160-L161

Useful? React with 👍 / 👎.

// Still unspent in chain and mempool long after the session completed -
// the finalized transaction most likely never propagated, release the
// input so the wallet does not lose it forever
LogPrintf("CCoinJoinClientManager::%s -- WARNING: never observed finalized mixing transaction for %s, "
"releasing lock after %d seconds\n",
__func__, outpoint.ToStringShort(), PENDING_OBSERVATION_TIMEOUT_SECONDS);
m_wallet->UnlockCoin(outpoint, &batch);
it = m_pending_obs.erase(it);
fChanged = true;
continue;
}
// Spent according to chain/mempool but the wallet has not recorded the
// spending transaction yet, keep waiting for it
it->second = nNow;
fChanged = true;
}
++it;
}
if (fChanged && !batch.WriteCoinJoinPendingObs(m_pending_obs)) {
LogPrintf("CCoinJoinClientManager::%s -- ERROR: failed to persist %d pending observation(s)\n", __func__,
m_pending_obs.size());
}
}

bool CCoinJoinClientManager::IsPendingObservation(const COutPoint& outpoint) const
{
AssertLockNotHeld(cs_pending_obs);
LOCK(cs_pending_obs);
return m_pending_obs.count(outpoint) > 0;
}

size_t CCoinJoinClientManager::GetPendingObservationCount() const
{
AssertLockNotHeld(cs_pending_obs);
LOCK(cs_pending_obs);
return m_pending_obs.size();
}

bool CCoinJoinClientManager::WaitForAnotherBlock() const
{
if (!m_mn_sync.IsBlockchainSynced()) return true;
Expand Down Expand Up @@ -1714,9 +1873,16 @@ void CCoinJoinClientManager::UpdatedBlockTip(const CBlockIndex* pindex)

void CCoinJoinClientManager::DoMaintenance(ChainstateManager& chainman, CConnman& connman, const CTxMemPool& mempool)
{
if (ShutdownRequested()) return;

// Run this before the mixing gates below: inputs of already completed sessions
// must be able to unlock even when mixing or CoinJoin itself is disabled,
// otherwise their persisted locks could linger forever
CheckPendingObservations(mempool);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Schedule pending-lock cleanup in blocks-only mode

When a wallet with pending observations is restarted with -blocksonly, this cleanup never executes: init.cpp constructs CJWalletManagerImpl with relay_txes=false, and CJWalletManagerImpl::Schedule returns without scheduling any maintenance in that mode. Consequently, even after the wallet observes the finalized transaction, its persisted pending locks and metadata remain indefinitely unless the user manually unlocks them. Schedule the wallet cleanup independently of relay/queue maintenance.

AGENTS.md reference: AGENTS.md:L169-L171

Useful? React with 👍 / 👎.


if (!CCoinJoinClientOptions::IsEnabled()) return;

if (!m_mn_sync.IsBlockchainSynced() || ShutdownRequested()) return;
if (!m_mn_sync.IsBlockchainSynced()) return;

static int nTick = 0;
static int nDoAutoNextRun = nTick + COINJOIN_AUTO_TIMEOUT_MIN;
Expand Down Expand Up @@ -1751,6 +1917,7 @@ UniValue CCoinJoinClientManager::getJsonInfo() const
{
UniValue obj(UniValue::VOBJ);
obj.pushKV("running", isMixing());
obj.pushKV("pending_inputs", static_cast<int64_t>(GetPendingObservationCount()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Load persisted observations before reporting their count

After loading a wallet that has persisted pending observations, m_pending_obs remains empty until CheckPendingObservations() lazily reads the database; because scheduleEvery performs its first cleanup run only after the one-minute interval, getcoinjoininfo incorrectly reports pending_inputs: 0 during that window even though the corresponding coins are already locked. Load the record before answering this RPC or initialize it when the client manager is created.

AGENTS.md reference: AGENTS.md:L169-L171

Useful? React with 👍 / 👎.


UniValue arrSessions(UniValue::VARR);
AssertLockNotHeld(cs_deqsessions);
Expand Down
36 changes: 34 additions & 2 deletions src/coinjoin/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
#include <util/translation.h>

#include <deque>
#include <map>
#include <memory>
#include <ranges>
#include <utility>

namespace wallet {
class WalletBatch;
} // namespace wallet

class CCoinJoinClientManager;
class CConnman;
class CDeterministicMNManager;
Expand Down Expand Up @@ -174,6 +179,21 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client
// how many blocks to wait for after one successful mixing tx in non-multisession mode
static constexpr int nMinBlocksToWait{1};

mutable Mutex cs_pending_obs;
//! Inputs of successfully completed mixing sessions which must stay locked until
//! the wallet observes the finalized mixing transaction spending them. Without this
//! a client which processes DSCOMPLETE(MSG_SUCCESS) before the (trickle-relayed)
//! finalized DSTX arrives could select the very same inputs for another session
//! (esp. with -coinjoinmultisession) and double-spend them.
//! Maps outpoint to the time it was added. Mirrored to the wallet database so that
//! both the locks and their grace period survive a restart, and so that these are
//! never confused with locks the user set themselves via `lockunspent`.
std::map<COutPoint, int64_t> m_pending_obs GUARDED_BY(cs_pending_obs);
bool m_pending_obs_loaded GUARDED_BY(cs_pending_obs){false};

/// Populate m_pending_obs from the wallet database, once per run
void LoadPendingObservations(wallet::WalletBatch& batch) EXCLUSIVE_LOCKS_REQUIRED(cs_pending_obs);

// Keep track of current block height
int nCachedBlockHeight{0};

Expand Down Expand Up @@ -211,15 +231,27 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client

void UpdatedSuccessBlock();

//! How long to keep waiting for the finalized mixing transaction before
//! double-checking chain/mempool spentness and potentially releasing the inputs
static constexpr int64_t PENDING_OBSERVATION_TIMEOUT_SECONDS{60 * 60};

/// Keep the given successfully mixed inputs locked (persistently) until the wallet
/// observes a transaction spending them
void AddPendingObservation(const std::vector<COutPoint>& outpoints) EXCLUSIVE_LOCKS_REQUIRED(!cs_pending_obs);
/// Release pending inputs whose spend has been observed by the wallet
void CheckPendingObservations(const CTxMemPool& mempool) EXCLUSIVE_LOCKS_REQUIRED(!cs_pending_obs);
bool IsPendingObservation(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(!cs_pending_obs);
size_t GetPendingObservationCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs_pending_obs);

void UpdatedBlockTip(const CBlockIndex* pindex);

void DoMaintenance(ChainstateManager& chainman, CConnman& connman, const CTxMemPool& mempool)
EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions, !cs_pending_obs);

// interfaces::CoinJoin::Client overrides
void disableAutobackups() override { fCreateAutoBackups = false; }
void resetPool() override EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
UniValue getJsonInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
UniValue getJsonInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions, !cs_pending_obs);
std::vector<std::string> getSessionStatuses() const override EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
std::string getSessionDenoms() const override EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
bool isMixing() const override;
Expand Down
21 changes: 20 additions & 1 deletion src/coinjoin/walletman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class CJWalletManagerImpl final : public CJWalletManager
std::map<const std::string, std::unique_ptr<CCoinJoinClientManager>> m_wallet_manager_map GUARDED_BY(cs_wallet_manager_map);

void DoMaintenance(CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map);
void CheckPendingObservations() EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map);

[[nodiscard]] MessageProcessingResult ProcessDSQueue(NodeId from, CConnman& connman, std::string_view msg_type,
CDataStream& vRecv) EXCLUSIVE_LOCKS_REQUIRED(!cs_ProcessDSQueue, !cs_wallet_manager_map);
Expand Down Expand Up @@ -119,7 +120,16 @@ CJWalletManagerImpl::~CJWalletManagerImpl()

void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
{
if (!m_relay_txes) return;
if (!m_relay_txes) {
// Mixing needs transaction relay, so no CoinJoin activity is scheduled here.
// Inputs of a session which completed before an earlier shutdown are a different
// matter: their locks were persisted and are restored with the wallet, so the
// check which releases them once their spend is observed (or once they time out)
// has to keep running even in block-only mode - nothing else would ever unlock them.
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::CheckPendingObservations, this),
std::chrono::minutes{1});
return;
}
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
std::chrono::seconds{1});
}
Expand Down Expand Up @@ -205,6 +215,15 @@ void CJWalletManagerImpl::DoMaintenance(CConnman& connman)
}
}

void CJWalletManagerImpl::CheckPendingObservations()
{
if (ShutdownRequested()) return;
LOCK(cs_wallet_manager_map);
for (auto& [_, clientman] : m_wallet_manager_map) {
clientman->CheckPendingObservations(m_mempool);
}
}

MessageProcessingResult CJWalletManagerImpl::processMessage(CNode& pfrom, Chainstate& chainstate, CConnman& connman,
CTxMemPool& mempool, std::string_view msg_type,
CDataStream& vRecv)
Expand Down
1 change: 1 addition & 0 deletions src/rpc/coinjoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ static RPCHelpMan getcoinjoininfo()
{RPCResult::Type::NUM, "denoms_hardcap", "Maximum limit of how many inputs of each denominated amount to create"},
{RPCResult::Type::NUM, "queue_size", "How many queues there are currently on the network"},
{RPCResult::Type::BOOL, "running", "Whether mixing is currently running"},
{RPCResult::Type::NUM, "pending_inputs", "The number of successfully mixed inputs kept locked until the transaction spending them is observed"},
{RPCResult::Type::ARR, "sessions", "",
{
{RPCResult::Type::OBJ, "", "",
Expand Down
Loading
Loading