Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 16 additions & 2 deletions src/llmq/quorums_instantsend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1393,12 +1393,26 @@ bool CInstantSendManager::RemoveISLockByTxId(const uint256& txid)
void CInstantSendManager::AskNodesForLockedTx(const uint256& txid)
{
std::vector<CNode*> nodesToAskFor;
g_connman->ForEachNode([&](CNode* pnode) {
LOCK(pnode->cs_filter);
nodesToAskFor.reserve(4);

auto maybe_add_to_nodesToAskFor = [&nodesToAskFor, &txid](CNode* pnode) {
if (nodesToAskFor.size() >= 4) {
return;
}
LOCK(pnode->cs_inventory);
if (pnode->filterInventoryKnown.contains(txid)) {
pnode->AddRef();
nodesToAskFor.emplace_back(pnode);
Comment on lines 1404 to 1405

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 Count only peers that accept the request

When any of the first four candidates has an over-limit vecAskFor or setAskFor—for example, after an inventory flood—CNode::AskFor returns without queuing anything (src/net.cpp:3568-3579), but this peer has already consumed one of the four vector slots. Since subsequent eligible peers are skipped once the vector reaches four, recovery can enqueue fewer than four, or even zero, requests and never ask an available fifth peer for the locked transaction; continue selecting until four requests are actually accepted.

AGENTS.md reference: AGENTS.md:L238-L238

Useful? React with 👍 / 👎.

}
};

g_connman->ForEachNode([&](CNode* pnode) {
// Check masternodes first
if (pnode->fZnode) maybe_add_to_nodesToAskFor(pnode);
});
g_connman->ForEachNode([&](CNode* pnode) {
// Check non-masternodes next
if (!pnode->fZnode) maybe_add_to_nodesToAskFor(pnode);
});
{
LOCK(cs_main);
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/quorums_instantsend.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class CInstantSendDb

class CInstantSendManager : public CRecoveredSigsListener
{
friend struct CInstantSendRequestTestAccess;

private:
CCriticalSection cs;
CInstantSendDb db;
Expand Down
1 change: 1 addition & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ add_executable(test_firo
${CMAKE_CURRENT_SOURCE_DIR}/random_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/reverselock_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/rpc_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/quorums_instantsend_request_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/sanity_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scheduler_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scriptnum10.h
Expand Down
93 changes: 93 additions & 0 deletions src/test/quorums_instantsend_request_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2026 The Firo developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "dbwrapper.h"
#include "llmq/quorums_instantsend.h"
#include "net.h"
#include "test/test_bitcoin.h"

#include <boost/test/unit_test.hpp>

#include <memory>
#include <vector>

namespace llmq
{

struct CInstantSendRequestTestAccess {
static void AskNodesForLockedTx(CInstantSendManager& manager, const uint256& txid)
{
manager.AskNodesForLockedTx(txid);
}
};

} // namespace llmq

namespace
{

struct InstantSendRequestSetup : BasicTestingSetup {
CDBWrapper db;
llmq::CInstantSendManager manager;
std::vector<std::unique_ptr<CNode> > nodes;

InstantSendRequestSetup() : db(boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(), 1 << 20, true, false),
manager(db)
{
g_connman = std::make_unique<CConnman>(0x1337, 0x1337);
}

~InstantSendRequestSetup()
{
LOCK(g_connman->cs_vNodes);
g_connman->vNodes.clear();
}

void AddNode(bool masternode, bool knowsTx, const uint256& txid)
{
auto node = std::make_unique<CNode>(nodes.size(), NODE_NETWORK, 0, INVALID_SOCKET, CAddress(), 0, 0);
node->fZnode = masternode;
node->fSuccessfullyConnected = true;
if (knowsTx) {
LOCK(node->cs_inventory);
node->filterInventoryKnown.insert(txid);
}
{
LOCK(g_connman->cs_vNodes);
g_connman->vNodes.emplace_back(node.get());
}
nodes.emplace_back(std::move(node));
}
};

} // namespace

BOOST_FIXTURE_TEST_SUITE(quorums_instantsend_request_tests, InstantSendRequestSetup)

BOOST_AUTO_TEST_CASE(asks_at_most_four_peers_and_prefers_masternodes)
{
uint256 txid;
txid.SetHex("01");

AddNode(false, true, txid);
AddNode(false, true, txid);
AddNode(false, true, txid);
AddNode(true, true, txid);
AddNode(true, false, txid);
AddNode(true, true, txid);
AddNode(true, true, txid);
AddNode(false, true, txid);

llmq::CInstantSendRequestTestAccess::AskNodesForLockedTx(manager, txid);

const std::vector<bool> expected{true, false, false, true, false, true, true, false};
for (size_t i = 0; i < nodes.size(); ++i) {
LOCK(nodes[i]->cs_inventory);
BOOST_CHECK_EQUAL(nodes[i]->setAskFor.count(txid), expected[i] ? 1 : 0);
BOOST_CHECK_EQUAL(nodes[i]->GetRefCount(), 0);
}
g_connman->RemoveAskFor(txid);
}

BOOST_AUTO_TEST_SUITE_END()