diff --git a/src/Makefile.test.include b/src/Makefile.test.include index d4107750889d6..c07649ebcc99c 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -137,6 +137,7 @@ BITCOIN_TESTS =\ test/llmq_commitment_tests.cpp \ test/llmq_hash_tests.cpp \ test/llmq_params_tests.cpp \ + test/llmq_signing_shares_tests.cpp \ test/llmq_snapshot_tests.cpp \ test/llmq_utils_tests.cpp \ test/logging_tests.cpp \ diff --git a/src/llmq/signing_shares.cpp b/src/llmq/signing_shares.cpp index fdb7d967dba42..b77e48ec42395 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -424,8 +424,9 @@ bool CSigSharesManager::ProcessMessageBatchedSigShares(const CNode& pfrom, const return true; } - if (bool ban{false}; !PreVerifyBatchedSigShares(m_mn_activeman, qman, sessionInfo, batchedSigShares, ban)) { - return !ban; + auto verifyResult = PreVerifyBatchedSigShares(m_mn_activeman, qman, sessionInfo, batchedSigShares); + if (!verifyResult.IsSuccess()) { + return !verifyResult.should_ban; } std::vector sigSharesToProcess; @@ -522,46 +523,43 @@ void CSigSharesManager::ProcessMessageSigShare(NodeId fromId, const CSigShare& s sigShare.GetSignHash().ToString(), sigShare.getId().ToString(), sigShare.getMsgHash().ToString(), sigShare.getQuorumMember(), fromId); } -bool CSigSharesManager::PreVerifyBatchedSigShares(const CActiveMasternodeManager& mn_activeman, const CQuorumManager& quorum_manager, - const CSigSharesNodeState::SessionInfo& session, const CBatchedSigShares& batchedSigShares, bool& retBan) +PreVerifyBatchedResult CSigSharesManager::PreVerifyBatchedSigShares(const CActiveMasternodeManager& mn_activeman, + const CQuorumManager& quorum_manager, + const CSigSharesNodeState::SessionInfo& session, + const CBatchedSigShares& batchedSigShares) { - retBan = false; - if (!IsQuorumActive(session.llmqType, quorum_manager, session.quorum->qc->quorumHash)) { // quorum is too old - return false; + return {PreVerifyResult::QuorumTooOld, false}; } if (!session.quorum->IsMember(mn_activeman.GetProTxHash())) { // we're not a member so we can't verify it (we actually shouldn't have received it) - return false; + return {PreVerifyResult::NotAMember, false}; } if (!session.quorum->HasVerificationVector()) { // TODO we should allow to ask other nodes for the quorum vvec if we missed it in the DKG LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- we don't have the quorum vvec for %s, no verification possible.\n", __func__, session.quorumHash.ToString()); - return false; + return {PreVerifyResult::MissingVerificationVector, false}; } std::unordered_set dupMembers; for (const auto& [quorumMember, _] : batchedSigShares.sigShares) { if (!dupMembers.emplace(quorumMember).second) { - retBan = true; - return false; + return {PreVerifyResult::DuplicateMember, true}; } if (quorumMember >= session.quorum->members.size()) { LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- quorumMember out of bounds\n", __func__); - retBan = true; - return false; + return {PreVerifyResult::QuorumMemberOutOfBounds, true}; } if (!session.quorum->qc->validMembers[quorumMember]) { LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- quorumMember not valid\n", __func__); - retBan = true; - return false; + return {PreVerifyResult::QuorumMemberNotValid, true}; } } - return true; + return {PreVerifyResult::Success, false}; } bool CSigSharesManager::CollectPendingSigSharesToVerify( diff --git a/src/llmq/signing_shares.h b/src/llmq/signing_shares.h index fb2f692306fa3..75a82bc8b0394 100644 --- a/src/llmq/signing_shares.h +++ b/src/llmq/signing_shares.h @@ -359,6 +359,23 @@ class CSignedSession int attempt{0}; }; +enum class PreVerifyResult { + Success, + QuorumTooOld, + NotAMember, + MissingVerificationVector, + DuplicateMember, + QuorumMemberOutOfBounds, + QuorumMemberNotValid +}; + +struct PreVerifyBatchedResult { + PreVerifyResult result; + bool should_ban; + + [[nodiscard]] bool IsSuccess() const { return result == PreVerifyResult::Success; } +}; + class CSigSharesManager : public CRecoveredSigsListener { private: @@ -447,6 +464,12 @@ class CSigSharesManager : public CRecoveredSigsListener void NotifyRecoveredSig(const std::shared_ptr& sig) const; + static bool VerifySigSharesInv(Consensus::LLMQType llmqType, const CSigSharesInv& inv); + static PreVerifyBatchedResult PreVerifyBatchedSigShares(const CActiveMasternodeManager& mn_activeman, + const CQuorumManager& quorum_manager, + const CSigSharesNodeState::SessionInfo& session, + const CBatchedSigShares& batchedSigShares); + private: // all of these return false when the currently processed message should be aborted (as each message actually contains multiple messages) bool ProcessMessageSigSesAnn(const CNode& pfrom, const CSigSesAnn& ann); @@ -455,10 +478,6 @@ class CSigSharesManager : public CRecoveredSigsListener bool ProcessMessageBatchedSigShares(const CNode& pfrom, const CBatchedSigShares& batchedSigShares); void ProcessMessageSigShare(NodeId fromId, const CSigShare& sigShare); - static bool VerifySigSharesInv(Consensus::LLMQType llmqType, const CSigSharesInv& inv); - static bool PreVerifyBatchedSigShares(const CActiveMasternodeManager& mn_activeman, const CQuorumManager& quorum_manager, - const CSigSharesNodeState::SessionInfo& session, const CBatchedSigShares& batchedSigShares, bool& retBan); - bool CollectPendingSigSharesToVerify( size_t maxUniqueSessions, std::unordered_map>& retSigShares, std::unordered_map, CQuorumCPtr, StaticSaltedHasher>& retQuorums); diff --git a/src/test/llmq_signing_shares_tests.cpp b/src/test/llmq_signing_shares_tests.cpp new file mode 100644 index 0000000000000..efd0d0b12242f --- /dev/null +++ b/src/test/llmq_signing_shares_tests.cpp @@ -0,0 +1,342 @@ +// Copyright (c) 2025 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace llmq; +using namespace llmq::testutils; + +// Test fixture with helper functions +struct LLMQSigningSharesTestFixture : public TestingSetup { + std::unique_ptr blsWorker; + CBLSSecretKey sk; + std::unique_ptr mn_activeman; + + LLMQSigningSharesTestFixture() : + TestingSetup(CBaseChainParams::REGTEST) + { + blsWorker = std::make_unique(); + + // Create active masternode manager with a test secret key + sk.MakeNewKey(); + mn_activeman = std::make_unique(sk, *Assert(m_node.connman), m_node.dmnman); + } + + // Helper to create a minimal test quorum + CQuorumCPtr CreateMinimalTestQuorum(int size, bool hasVerificationVector = true, + const std::vector& validMembers = {}, bool includeOurProTxHash = false) + { + const auto& params = GetLLMQParams(Consensus::LLMQType::LLMQ_TEST_V17); + + auto quorum = std::make_shared(params, *blsWorker); + + // Create commitment + auto qc_ptr = std::make_unique(); + qc_ptr->llmqType = params.type; + qc_ptr->quorumHash = InsecureRand256(); + + // Set valid members + if (!validMembers.empty()) { + qc_ptr->validMembers = validMembers; + } else { + qc_ptr->validMembers.resize(size, true); + } + + // Create members + std::vector members; + for (int i = 0; i < size; ++i) { + // For simplicity, use nullptr members since we're testing pre-verification logic + // The actual member checking happens in IsMember() which we can't fully mock + members.push_back(nullptr); + } + + quorum->Init(std::move(qc_ptr), nullptr, InsecureRand256(), members); + + // Set verification vector if requested + if (hasVerificationVector) { + std::vector vvec; + for (int i = 0; i < size; ++i) { + CBLSSecretKey sk; + sk.MakeNewKey(); + vvec.push_back(sk.GetPublicKey()); + } + quorum->SetVerificationVector(vvec); + } + + return quorum; + } + + // Helper to create test SessionInfo + CSigSharesNodeState::SessionInfo CreateTestSessionInfo(CQuorumCPtr quorum) + { + CSigSharesNodeState::SessionInfo session; + session.llmqType = quorum->params.type; + session.quorumHash = quorum->qc->quorumHash; + session.id = InsecureRand256(); + session.msgHash = InsecureRand256(); + session.quorum = quorum; + return session; + } + + // Helper to create test BatchedSigShares + CBatchedSigShares CreateTestBatchedSigShares(const std::vector& members) + { + CBatchedSigShares batched; + batched.sessionId = 1; + + for (uint16_t member : members) { + CBLSLazySignature lazySig; + batched.sigShares.emplace_back(member, lazySig); + } + + return batched; + } +}; + +BOOST_FIXTURE_TEST_SUITE(llmq_signing_shares_tests, LLMQSigningSharesTestFixture) + +// Test: Missing verification vector +// Note: This test will likely return QuorumTooOld or NotAMember because we can't fully mock +// IsQuorumActive and IsMember. However, we can still verify the function is callable and +// returns a proper PreVerifyBatchedResult. +BOOST_AUTO_TEST_CASE(preverify_missing_verification_vector) +{ + // Create quorum WITHOUT verification vector + auto quorum = CreateMinimalTestQuorum(3, false); + auto sessionInfo = CreateTestSessionInfo(quorum); + auto batchedSigShares = CreateTestBatchedSigShares({0, 1}); + + // Call PreVerifyBatchedSigShares - it should detect missing verification vector + // (if it gets past the earlier checks for quorum activity and membership) + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect it to fail (not be successful) + BOOST_CHECK(!result.IsSuccess()); + // The actual result will likely be QuorumTooOld, NotAMember, or MissingVerificationVector + // depending on which check fails first +} + +// Test: Duplicate member detection +// Since we control the batched sig shares structure, we can test this validation +// even if earlier checks might fail +BOOST_AUTO_TEST_CASE(preverify_duplicate_member) +{ + // Create a valid quorum + auto quorum = CreateMinimalTestQuorum(5, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch with duplicate member (0 appears twice) + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 0, 2}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect failure + BOOST_CHECK(!result.IsSuccess()); + + // If we get to the duplicate check (past QuorumTooOld and NotAMember checks), + // we should get DuplicateMember with should_ban=true + if (result.result == PreVerifyResult::DuplicateMember) { + BOOST_CHECK(result.should_ban); + } +} + +// Test: Quorum member out of bounds +BOOST_AUTO_TEST_CASE(preverify_member_out_of_bounds) +{ + // Create quorum with 5 members + auto quorum = CreateMinimalTestQuorum(5, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch with member index out of bounds (>= 5) + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 10}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect failure + BOOST_CHECK(!result.IsSuccess()); + + // If we get to the bounds check, we should get QuorumMemberOutOfBounds with should_ban=true + if (result.result == PreVerifyResult::QuorumMemberOutOfBounds) { + BOOST_CHECK(result.should_ban); + } +} + +// Test: Invalid quorum member +BOOST_AUTO_TEST_CASE(preverify_invalid_quorum_member) +{ + // Create quorum with specific valid members pattern + std::vector validMembers = {true, false, true, true, false}; + auto quorum = CreateMinimalTestQuorum(5, true, validMembers); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch including an invalid member (member 1 is invalid) + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect failure + BOOST_CHECK(!result.IsSuccess()); + + // If we get to the valid member check, we should get QuorumMemberNotValid with should_ban=true + if (result.result == PreVerifyResult::QuorumMemberNotValid) { + BOOST_CHECK(result.should_ban); + } +} + +// Test: Valid batch structure (though may fail early checks) +BOOST_AUTO_TEST_CASE(preverify_valid_batch_structure) +{ + // Create a valid quorum + auto quorum = CreateMinimalTestQuorum(5, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create a valid batch (all members exist and are unique) + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2, 3, 4}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // The batch structure is valid, but we may fail on QuorumTooOld or NotAMember + // This test ensures that valid batch structure doesn't cause crashes + // and that the function returns a proper result + + // Verify that at least we get a proper result type + // (not a ban-worthy failure from structure validation) + if (!result.IsSuccess()) { + // If not successful, it should be due to quorum checks, not structure validation + BOOST_CHECK(result.result == PreVerifyResult::QuorumTooOld || result.result == PreVerifyResult::NotAMember || + result.result == PreVerifyResult::MissingVerificationVector); + } +} + +// Test: Empty batch +BOOST_AUTO_TEST_CASE(preverify_empty_batch) +{ + // Create a valid quorum + auto quorum = CreateMinimalTestQuorum(5, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create an empty batch + auto batchedSigShares = CreateTestBatchedSigShares({}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // Empty batch should not trigger ban-worthy errors related to member validation + // It may fail early checks (QuorumTooOld, NotAMember), but shouldn't trigger + // DuplicateMember, QuorumMemberOutOfBounds, or QuorumMemberNotValid + if (!result.IsSuccess()) { + BOOST_CHECK(result.result != PreVerifyResult::DuplicateMember); + BOOST_CHECK(result.result != PreVerifyResult::QuorumMemberOutOfBounds); + BOOST_CHECK(result.result != PreVerifyResult::QuorumMemberNotValid); + } +} + +// Test: Multiple duplicates +BOOST_AUTO_TEST_CASE(preverify_multiple_duplicates) +{ + auto quorum = CreateMinimalTestQuorum(10, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch with multiple duplicates - should fail on first duplicate found + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2, 1, 3, 2, 4}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect failure + BOOST_CHECK(!result.IsSuccess()); + + // If we get to the duplicate check, it should trigger DuplicateMember with ban + if (result.result == PreVerifyResult::DuplicateMember) { + BOOST_CHECK(result.should_ban); + } +} + +// Test: Boundary case - maximum member index +BOOST_AUTO_TEST_CASE(preverify_boundary_max_member) +{ + const int quorum_size = 10; + auto quorum = CreateMinimalTestQuorum(quorum_size, true); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch with last valid member (size - 1) + auto batchedSigShares = CreateTestBatchedSigShares({0, static_cast(quorum_size - 1)}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // This should not trigger QuorumMemberOutOfBounds since the max index is valid + if (!result.IsSuccess()) { + BOOST_CHECK(result.result != PreVerifyResult::QuorumMemberOutOfBounds); + } +} + +// Test: All members invalid scenario +BOOST_AUTO_TEST_CASE(preverify_all_members_invalid) +{ + // Create quorum where all members are invalid + std::vector validMembers(5, false); + auto quorum = CreateMinimalTestQuorum(5, true, validMembers); + auto sessionInfo = CreateTestSessionInfo(quorum); + + // Create batch with any members - they're all invalid + auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2}); + + // Call PreVerifyBatchedSigShares + auto result = CSigSharesManager::PreVerifyBatchedSigShares(*mn_activeman, *Assert(m_node.llmq_ctx->qman), + sessionInfo, batchedSigShares); + + // We expect failure + BOOST_CHECK(!result.IsSuccess()); + + // If we get to the valid member check, should trigger QuorumMemberNotValid with ban + if (result.result == PreVerifyResult::QuorumMemberNotValid) { + BOOST_CHECK(result.should_ban); + } +} + +// Test: Result enum values and should_ban flag +BOOST_AUTO_TEST_CASE(preverify_result_structure) +{ + // Test that PreVerifyBatchedResult works correctly + PreVerifyBatchedResult success_result{PreVerifyResult::Success, false}; + BOOST_CHECK(success_result.IsSuccess()); + BOOST_CHECK(!success_result.should_ban); + + PreVerifyBatchedResult ban_result{PreVerifyResult::DuplicateMember, true}; + BOOST_CHECK(!ban_result.IsSuccess()); + BOOST_CHECK(ban_result.should_ban); + + PreVerifyBatchedResult no_ban_result{PreVerifyResult::QuorumTooOld, false}; + BOOST_CHECK(!no_ban_result.IsSuccess()); + BOOST_CHECK(!no_ban_result.should_ban); +} + +BOOST_AUTO_TEST_SUITE_END()