LCOV - code coverage report
Current view: top level - src/test - llmq_commitment_tests.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 221 223 99.1 %
Date: 2026-06-25 07:23:43 Functions: 87 87 100.0 %

          Line data    Source code
       1             : // Copyright (c) 2025 The Dash Core developers
       2             : // Distributed under the MIT software license, see the accompanying
       3             : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
       4             : 
       5             : #include <test/util/llmq_tests.h>
       6             : #include <test/util/setup_common.h>
       7             : 
       8             : #include <chain.h>
       9             : #include <consensus/validation.h>
      10             : #include <evo/specialtx.h>
      11             : #include <llmq/commitment.h>
      12             : #include <llmq/context.h>
      13             : #include <llmq/utils.h>
      14             : #include <logging.h>
      15             : #include <node/context.h>
      16             : #include <primitives/transaction.h>
      17             : #include <streams.h>
      18             : #include <util/check.h>
      19             : #include <util/strencodings.h>
      20             : 
      21             : #include <boost/test/unit_test.hpp>
      22             : 
      23             : #include <algorithm>
      24             : 
      25             : using namespace llmq;
      26             : using namespace llmq::testutils;
      27             : 
      28         146 : BOOST_FIXTURE_TEST_SUITE(llmq_commitment_tests, BasicTestingSetup)
      29             : 
      30             : // Get test params for use in tests
      31         146 : static const Consensus::LLMQParams& TEST_PARAMS = GetLLMQParams(Consensus::LLMQType::LLMQ_TEST_V17);
      32             : 
      33         149 : BOOST_AUTO_TEST_CASE(commitment_null_test)
      34             : {
      35           1 :     CFinalCommitment commitment;
      36             : 
      37             :     // Test default constructor creates null commitment
      38           1 :     BOOST_CHECK(commitment.IsNull());
      39           1 :     BOOST_CHECK(commitment.quorumHash.IsNull());
      40           1 :     BOOST_CHECK(commitment.validMembers.empty());
      41           1 :     BOOST_CHECK(commitment.signers.empty());
      42           1 :     BOOST_CHECK(!commitment.quorumPublicKey.IsValid());
      43           1 :     BOOST_CHECK(!commitment.quorumSig.IsValid());
      44             : 
      45             :     // Note: VerifyNull requires valid LLMQ params which we can't test in unit tests
      46             :     // It's tested in functional tests
      47           1 : }
      48             : 
      49         149 : BOOST_AUTO_TEST_CASE(commitment_counting_test)
      50             : {
      51           1 :     CFinalCommitment commitment;
      52             : 
      53             :     // Test empty vectors
      54           1 :     BOOST_CHECK_EQUAL(commitment.CountSigners(), 0);
      55           1 :     BOOST_CHECK_EQUAL(commitment.CountValidMembers(), 0);
      56             : 
      57             :     // Test with various patterns
      58           1 :     commitment.signers = {true, false, true, true, false};
      59           1 :     commitment.validMembers = {true, true, false, true, true};
      60             : 
      61           1 :     BOOST_CHECK_EQUAL(commitment.CountSigners(), 3);
      62           1 :     BOOST_CHECK_EQUAL(commitment.CountValidMembers(), 4);
      63             : 
      64             :     // Test all true
      65           1 :     commitment.signers = std::vector<bool>(10, true);
      66           1 :     commitment.validMembers = std::vector<bool>(10, true);
      67             : 
      68           1 :     BOOST_CHECK_EQUAL(commitment.CountSigners(), 10);
      69           1 :     BOOST_CHECK_EQUAL(commitment.CountValidMembers(), 10);
      70             : 
      71             :     // Test all false
      72           1 :     commitment.signers = std::vector<bool>(10, false);
      73           1 :     commitment.validMembers = std::vector<bool>(10, false);
      74             : 
      75           1 :     BOOST_CHECK_EQUAL(commitment.CountSigners(), 0);
      76           1 :     BOOST_CHECK_EQUAL(commitment.CountValidMembers(), 0);
      77           1 : }
      78             : 
      79         149 : BOOST_AUTO_TEST_CASE(commitment_verify_sizes_test)
      80             : {
      81           1 :     CFinalCommitment commitment;
      82           1 :     commitment.llmqType = TEST_PARAMS.type;
      83             : 
      84             :     // Test with incorrect sizes (TEST_PARAMS.size is 3, so use a different size)
      85           1 :     commitment.validMembers = std::vector<bool>(5, true);
      86           1 :     commitment.signers = std::vector<bool>(5, true);
      87           1 :     BOOST_CHECK(!commitment.VerifySizes(TEST_PARAMS));
      88             : 
      89             :     // Test with correct sizes
      90           1 :     commitment.validMembers = std::vector<bool>(TEST_PARAMS.size, true);
      91           1 :     commitment.signers = std::vector<bool>(TEST_PARAMS.size, true);
      92           1 :     BOOST_CHECK(commitment.VerifySizes(TEST_PARAMS));
      93             : 
      94             :     // Test with mismatched sizes
      95           1 :     commitment.validMembers = std::vector<bool>(TEST_PARAMS.size, true);
      96           1 :     commitment.signers = std::vector<bool>(TEST_PARAMS.size + 1, true);
      97           1 :     BOOST_CHECK(!commitment.VerifySizes(TEST_PARAMS));
      98           1 : }
      99             : 
     100         149 : BOOST_FIXTURE_TEST_CASE(commitment_check_undersized_bitset_debug_log_test, RegTestingSetup)
     101             : {
     102             :     // Catches the OOB-read regression in CheckLLMQCommitment's debug-log loop
     103             :     // by capturing log output rather than relying on undefined behaviour to
     104             :     // trip a sanitizer. The wire-format validMembers DYNBITSET can deserialize
     105             :     // smaller than llmq_params.size; before the clamp the loop iterated up to
     106             :     // llmq_params.size and emitted v[0], v[1], ... reading past the bitset.
     107             :     // With the clamp an empty bitset must produce "validMembers[]".
     108             :     struct LogCaptureGuard {
     109           1 :         const uint64_t saved_categories{LogInstance().GetCategoryMask()};
     110             :         std::list<std::function<void(const std::string&)>>::iterator it;
     111           3 :         explicit LogCaptureGuard(std::vector<std::string>& sink)
     112           1 :         {
     113           1 :             LogInstance().EnableCategory(BCLog::LLMQ);
     114           2 :             it = LogInstance().PushBackCallback(
     115           3 :                 [&sink](const std::string& msg) { sink.push_back(msg); });
     116           2 :         }
     117           2 :         ~LogCaptureGuard()
     118           1 :         {
     119           1 :             LogInstance().DeleteCallback(it);
     120           1 :             if (!(saved_categories & BCLog::LLMQ)) {
     121           0 :                 LogInstance().DisableCategory(BCLog::LLMQ);
     122           0 :             }
     123           2 :         }
     124             :     };
     125             : 
     126           1 :     std::vector<std::string> log_lines;
     127           1 :     LogCaptureGuard guard{log_lines};
     128           1 :     BOOST_REQUIRE(LogAcceptDebug(BCLog::LLMQ));
     129             : 
     130           1 :     CFinalCommitmentTxPayload payload;
     131           1 :     payload.nVersion = CFinalCommitmentTxPayload::CURRENT_VERSION;
     132             :     // base_index below sits at height 0, so CheckLLMQCommitment expects
     133             :     // qcTx.nHeight == 1. Setting it to 2 forces the function to fail with
     134             :     // "bad-qc-height" after the pre-validation debug-log block but before
     135             :     // LookupBlockIndex / Verify / VerifySizes, which is the path the clamp
     136             :     // is meant to harden.
     137           1 :     payload.nHeight = 2;
     138           1 :     payload.commitment.nVersion = CFinalCommitment::LEGACY_BLS_NON_INDEXED_QUORUM_VERSION;
     139           1 :     payload.commitment.llmqType = TEST_PARAMS.type;
     140           1 :     payload.commitment.quorumHash = GetTestQuorumHash(1);
     141             :     // TEST_PARAMS.size is 3; an empty bitset models a malformed mined commitment.
     142           1 :     payload.commitment.signers      = std::vector<bool>();
     143           1 :     payload.commitment.validMembers = std::vector<bool>();
     144             : 
     145           1 :     CMutableTransaction mtx;
     146           1 :     mtx.nVersion = CTransaction::SPECIAL_VERSION;
     147           1 :     mtx.nType    = TRANSACTION_QUORUM_COMMITMENT;
     148           1 :     SetTxPayload(mtx, payload);
     149           1 :     const CTransaction tx{mtx};
     150             : 
     151             :     // Sanity check that the undersized bitsets survive payload round-trip,
     152             :     // so the call below is genuinely exercising the OOB-prone code path.
     153           1 :     const auto opt_round_trip = GetTxPayload<CFinalCommitmentTxPayload>(tx);
     154           1 :     BOOST_REQUIRE(opt_round_trip.has_value());
     155           1 :     BOOST_CHECK_LT(opt_round_trip->commitment.validMembers.size(), static_cast<size_t>(TEST_PARAMS.size));
     156           1 :     BOOST_CHECK_LT(opt_round_trip->commitment.signers.size(),     static_cast<size_t>(TEST_PARAMS.size));
     157             : 
     158           1 :     CBlockIndex base_index;
     159           1 :     base_index.nHeight = 0;
     160             : 
     161           1 :     const llmq::UtilParameters util_params{*Assert(m_node.dmnman),
     162           1 :                                            *Assert(m_node.llmq_ctx)->qsnapman,
     163           1 :                                            *Assert(m_node.chainman),
     164           1 :                                            &base_index};
     165             : 
     166           1 :     TxValidationState state;
     167           1 :     BOOST_CHECK(!llmq::CheckLLMQCommitment(util_params, tx, state));
     168           1 :     BOOST_CHECK(state.IsInvalid());
     169           1 :     BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-qc-height");
     170             : 
     171             :     // Locate the validMembers debug line emitted by CheckLLMQCommitment and
     172             :     // assert the loop was clamped: with an empty bitset the rendered list must
     173             :     // be empty. Old code emitted "v[0]=..." here even though the bitset had no
     174             :     // elements, so checking for the absence of "v[0]" pins down the regression.
     175           1 :     const std::string marker = "CFinalCommitment -- CheckLLMQCommitment";
     176           2 :     const auto it = std::find_if(log_lines.begin(), log_lines.end(),
     177           2 :                                  [&marker](const std::string& s) {
     178           1 :                                      return s.find(marker) != std::string::npos;
     179             :                                  });
     180           1 :     BOOST_REQUIRE_MESSAGE(it != log_lines.end(),
     181             :                           "CheckLLMQCommitment debug line not captured");
     182           1 :     BOOST_CHECK_MESSAGE(it->find("validMembers[]") != std::string::npos,
     183             :                         "expected clamped 'validMembers[]', got: " + *it);
     184           1 :     BOOST_CHECK_MESSAGE(it->find("v[0]") == std::string::npos,
     185             :                         "unexpected v[0] in clamped log line: " + *it);
     186           1 : }
     187             : 
     188         149 : BOOST_AUTO_TEST_CASE(commitment_serialization_test)
     189             : {
     190             :     // Test with valid commitment
     191           1 :     CFinalCommitment commitment = CreateValidCommitment(TEST_PARAMS, GetTestQuorumHash(1));
     192             : 
     193             :     // Test serialization preserves all fields
     194           1 :     CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
     195           1 :     ss << commitment;
     196             : 
     197           1 :     CFinalCommitment deserialized;
     198           1 :     ss >> deserialized;
     199             : 
     200           1 :     BOOST_CHECK_EQUAL(commitment.llmqType, deserialized.llmqType);
     201           1 :     BOOST_CHECK(commitment.quorumHash == deserialized.quorumHash);
     202           1 :     BOOST_CHECK(commitment.validMembers == deserialized.validMembers);
     203           1 :     BOOST_CHECK(commitment.signers == deserialized.signers);
     204           1 :     BOOST_CHECK(commitment.quorumVvecHash == deserialized.quorumVvecHash);
     205           1 :     BOOST_CHECK(commitment.quorumPublicKey == deserialized.quorumPublicKey);
     206           1 :     BOOST_CHECK(commitment.quorumSig == deserialized.quorumSig);
     207           1 :     BOOST_CHECK(commitment.membersSig == deserialized.membersSig);
     208           1 :     BOOST_CHECK_EQUAL(commitment.IsNull(), deserialized.IsNull());
     209           1 : }
     210             : 
     211         149 : BOOST_AUTO_TEST_CASE(commitment_version_test)
     212             : {
     213             :     // Test version calculation (first param is rotation enabled, second is basic scheme active)
     214             :     // With rotation enabled and basic scheme
     215           1 :     BOOST_CHECK_EQUAL(CFinalCommitment::GetVersion(true, true), CFinalCommitment::BASIC_BLS_INDEXED_QUORUM_VERSION);
     216             :     // With rotation enabled but legacy scheme
     217           1 :     BOOST_CHECK_EQUAL(CFinalCommitment::GetVersion(true, false), CFinalCommitment::LEGACY_BLS_INDEXED_QUORUM_VERSION);
     218             :     // Without rotation but basic scheme
     219           1 :     BOOST_CHECK_EQUAL(CFinalCommitment::GetVersion(false, true), CFinalCommitment::BASIC_BLS_NON_INDEXED_QUORUM_VERSION);
     220             :     // Without rotation and legacy scheme
     221           1 :     BOOST_CHECK_EQUAL(CFinalCommitment::GetVersion(false, false), CFinalCommitment::LEGACY_BLS_NON_INDEXED_QUORUM_VERSION);
     222           1 : }
     223             : 
     224         149 : BOOST_AUTO_TEST_CASE(commitment_json_test)
     225             : {
     226           1 :     CFinalCommitment commitment = CreateValidCommitment(TEST_PARAMS, GetTestQuorumHash(1));
     227             : 
     228           1 :     UniValue json = commitment.ToJson();
     229             : 
     230             :     // Verify JSON contains expected fields
     231           1 :     BOOST_CHECK(json.exists("llmqType"));
     232           1 :     BOOST_CHECK(json.exists("quorumHash"));
     233           1 :     BOOST_CHECK(json.exists("signers"));
     234           1 :     BOOST_CHECK(json.exists("validMembers"));
     235           1 :     BOOST_CHECK(json.exists("quorumPublicKey"));
     236           1 :     BOOST_CHECK(json.exists("quorumVvecHash"));
     237           1 :     BOOST_CHECK(json.exists("quorumSig"));
     238           1 :     BOOST_CHECK(json.exists("membersSig"));
     239             : 
     240             :     // Verify counts are included
     241           1 :     BOOST_CHECK(json.exists("signersCount"));
     242           1 :     BOOST_CHECK(json.exists("validMembersCount"));
     243             : 
     244           1 :     BOOST_CHECK_EQUAL(json["signersCount"].getInt<int>(), commitment.CountSigners());
     245           1 :     BOOST_CHECK_EQUAL(json["validMembersCount"].getInt<int>(), commitment.CountValidMembers());
     246           1 : }
     247             : 
     248         149 : BOOST_AUTO_TEST_CASE(commitment_bitvector_json_test)
     249             : {
     250             :     // Test bit vector serialization through JSON output
     251           1 :     CFinalCommitment commitment;
     252           1 :     commitment.llmqType = TEST_PARAMS.type;
     253           1 :     commitment.quorumHash = GetTestQuorumHash(1);
     254             : 
     255             :     // Test empty vectors
     256           1 :     commitment.validMembers.clear();
     257           1 :     commitment.signers.clear();
     258           1 :     UniValue json = commitment.ToJson();
     259           1 :     BOOST_CHECK_EQUAL(json["validMembers"].get_str(), "");
     260           1 :     BOOST_CHECK_EQUAL(json["signers"].get_str(), "");
     261             : 
     262             :     // Test single byte patterns
     263           1 :     commitment.validMembers = std::vector<bool>(8, false);
     264           1 :     commitment.signers = std::vector<bool>(8, false);
     265           1 :     json = commitment.ToJson();
     266           1 :     BOOST_CHECK_EQUAL(json["validMembers"].get_str(), "00");
     267           1 :     BOOST_CHECK_EQUAL(json["signers"].get_str(), "00");
     268             : 
     269           1 :     commitment.validMembers = std::vector<bool>(8, true);
     270           1 :     commitment.signers = std::vector<bool>(8, true);
     271           1 :     json = commitment.ToJson();
     272           1 :     BOOST_CHECK_EQUAL(json["validMembers"].get_str(), "ff");
     273           1 :     BOOST_CHECK_EQUAL(json["signers"].get_str(), "ff");
     274             : 
     275             :     // Test specific patterns
     276             :     // Note: Bit order in serialization is LSB first within each byte
     277           1 :     commitment.validMembers = {true, false, true, false, true, false, true, false}; // 0x55 (01010101 in LSB)
     278           1 :     commitment.signers = {false, true, false, true, false, true, false, true};      // 0xAA (10101010 in LSB)
     279           1 :     json = commitment.ToJson();
     280           1 :     BOOST_CHECK_EQUAL(json["validMembers"].get_str(), "55");
     281           1 :     BOOST_CHECK_EQUAL(json["signers"].get_str(), "aa");
     282             : 
     283             :     // Test non-byte-aligned sizes (should pad with zeros)
     284           1 :     commitment.validMembers = {true, true, true, true, true}; // 0x1F padded
     285           1 :     commitment.signers = commitment.validMembers;
     286           1 :     json = commitment.ToJson();
     287           1 :     BOOST_CHECK_EQUAL(json["validMembers"].get_str(), "1f");
     288           1 :     BOOST_CHECK_EQUAL(json["signers"].get_str(), "1f");
     289           1 : }
     290             : 
     291         149 : BOOST_AUTO_TEST_CASE(commitment_verify_null_edge_cases)
     292             : {
     293           1 :     CFinalCommitment commitment;
     294             : 
     295             :     // Fresh commitment should be null
     296           1 :     BOOST_CHECK(commitment.IsNull());
     297             : 
     298             :     // Setting quorumHash alone doesn't make it non-null
     299             :     // (IsNull() doesn't check quorumHash)
     300           1 :     commitment.quorumHash = GetTestQuorumHash(1);
     301           1 :     BOOST_CHECK(commitment.IsNull());
     302           1 :     commitment.quorumHash.SetNull();
     303             : 
     304             :     // Setting llmqType alone doesn't make it non-null
     305           1 :     commitment.llmqType = Consensus::LLMQType::LLMQ_TEST;
     306           1 :     BOOST_CHECK(commitment.IsNull());
     307           1 :     commitment.llmqType = Consensus::LLMQType::LLMQ_NONE;
     308             : 
     309             :     // Setting validMembers with true values makes it non-null
     310           1 :     commitment.validMembers = {true};
     311           1 :     BOOST_CHECK(!commitment.IsNull());
     312           1 :     commitment.validMembers.clear();
     313             : 
     314             :     // Setting signers with only false values keeps it null
     315           1 :     commitment.signers = {false};
     316           1 :     BOOST_CHECK(commitment.IsNull());
     317             : 
     318             :     // Setting signers with true values makes it non-null
     319           1 :     commitment.signers = {true};
     320           1 :     BOOST_CHECK(!commitment.IsNull());
     321           1 :     commitment.signers.clear();
     322             : 
     323             :     // Setting quorumPublicKey makes it non-null
     324           1 :     commitment.quorumPublicKey = CreateRandomBLSPublicKey();
     325           1 :     BOOST_CHECK(!commitment.IsNull());
     326             : 
     327             :     // Reset and test quorumVvecHash
     328           1 :     commitment = CFinalCommitment{};
     329           1 :     commitment.quorumVvecHash = GetTestQuorumHash(2);
     330           1 :     BOOST_CHECK(!commitment.IsNull());
     331             : 
     332             :     // Reset and test signatures
     333           1 :     commitment = CFinalCommitment{};
     334           1 :     commitment.membersSig = CreateRandomBLSSignature();
     335           1 :     BOOST_CHECK(!commitment.IsNull());
     336             : 
     337           1 :     commitment = CFinalCommitment{};
     338           1 :     commitment.quorumSig = CreateRandomBLSSignature();
     339           1 :     BOOST_CHECK(!commitment.IsNull());
     340           1 : }
     341             : 
     342         149 : BOOST_AUTO_TEST_CASE(commitment_tx_payload_test)
     343             : {
     344           1 :     CFinalCommitmentTxPayload payload;
     345           1 :     payload.nHeight = 12345;
     346           1 :     payload.commitment = CreateValidCommitment(TEST_PARAMS, GetTestQuorumHash(1));
     347             : 
     348             :     // Test basic construction
     349           1 :     BOOST_CHECK_EQUAL(payload.nVersion, CFinalCommitmentTxPayload::CURRENT_VERSION);
     350           1 :     BOOST_CHECK_EQUAL(payload.nHeight, 12345);
     351           1 :     BOOST_CHECK(!payload.commitment.IsNull());
     352           1 : }
     353             : 
     354         149 : BOOST_AUTO_TEST_CASE(build_commitment_hash_test)
     355             : {
     356             :     // Test deterministic hash generation
     357           2 :     uint256 hash1 = llmq::BuildCommitmentHash(TEST_PARAMS.type, GetTestQuorumHash(1),
     358           1 :                                               CreateBitVector(TEST_PARAMS.size, {0, 1, 2}), CreateRandomBLSPublicKey(),
     359           1 :                                               GetTestQuorumHash(2));
     360             : 
     361             :     // Same inputs should produce same hash
     362           2 :     uint256 hash2 = llmq::BuildCommitmentHash(TEST_PARAMS.type, GetTestQuorumHash(1),
     363           1 :                                               CreateBitVector(TEST_PARAMS.size, {0, 1, 2}), CreateRandomBLSPublicKey(),
     364           1 :                                               GetTestQuorumHash(2));
     365             : 
     366             :     // Different quorum hash should produce different hash
     367           2 :     uint256 hash3 = llmq::BuildCommitmentHash(TEST_PARAMS.type,
     368           1 :                                               GetTestQuorumHash(2), // Different
     369           1 :                                               CreateBitVector(TEST_PARAMS.size, {0, 1, 2}), CreateRandomBLSPublicKey(),
     370           1 :                                               GetTestQuorumHash(2));
     371             : 
     372           1 :     BOOST_CHECK(hash1 != hash2); // Different pubkeys
     373           1 :     BOOST_CHECK(hash1 != hash3);
     374           1 :     BOOST_CHECK(hash2 != hash3);
     375             : 
     376             :     // Test with same deterministic data
     377           1 :     CBLSPublicKey fixedPubKey = CreateRandomBLSPublicKey();
     378           2 :     uint256 hash4 = llmq::BuildCommitmentHash(TEST_PARAMS.type, GetTestQuorumHash(1),
     379           1 :                                               CreateBitVector(TEST_PARAMS.size, {0, 1, 2}), fixedPubKey,
     380           1 :                                               GetTestQuorumHash(2));
     381             : 
     382           2 :     uint256 hash5 = llmq::BuildCommitmentHash(TEST_PARAMS.type, GetTestQuorumHash(1),
     383           1 :                                               CreateBitVector(TEST_PARAMS.size, {0, 1, 2}), fixedPubKey,
     384           1 :                                               GetTestQuorumHash(2));
     385             : 
     386           1 :     BOOST_CHECK(hash4 == hash5);
     387           1 : }
     388             : 
     389         146 : BOOST_AUTO_TEST_SUITE_END()

Generated by: LCOV version 1.16