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()
|