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 <streams.h>
10 : #include <univalue.h>
11 :
12 : #include <llmq/params.h>
13 : #include <llmq/snapshot.h>
14 :
15 : #include <boost/test/unit_test.hpp>
16 :
17 : #include <vector>
18 :
19 : using namespace llmq;
20 : using namespace llmq::testutils;
21 :
22 146 : BOOST_FIXTURE_TEST_SUITE(llmq_snapshot_tests, BasicTestingSetup)
23 :
24 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_construction_test)
25 : {
26 : // Test default constructor
27 1 : CQuorumSnapshot snapshot1;
28 1 : BOOST_CHECK(snapshot1.activeQuorumMembers.empty());
29 1 : BOOST_CHECK_EQUAL(snapshot1.mnSkipListMode, SnapshotSkipMode::MODE_NO_SKIPPING);
30 1 : BOOST_CHECK(snapshot1.mnSkipList.empty());
31 :
32 : // Test parameterized constructor
33 1 : std::vector<bool> activeMembers = {true, false, true, true, false};
34 1 : auto skipMode = SnapshotSkipMode::MODE_SKIPPING_ENTRIES;
35 1 : std::vector<int> skipList = {1, 3, 5, 7};
36 :
37 1 : CQuorumSnapshot snapshot2(activeMembers, skipMode, skipList);
38 1 : BOOST_CHECK(snapshot2.activeQuorumMembers == activeMembers);
39 1 : BOOST_CHECK_EQUAL(snapshot2.mnSkipListMode, skipMode);
40 1 : BOOST_CHECK(snapshot2.mnSkipList == skipList);
41 :
42 : // Test move semantics
43 1 : std::vector<bool> activeMembersCopy = activeMembers;
44 1 : std::vector<int> skipListCopy = skipList;
45 1 : CQuorumSnapshot snapshot3(std::move(activeMembersCopy), skipMode, std::move(skipListCopy));
46 1 : BOOST_CHECK(snapshot3.activeQuorumMembers == activeMembers);
47 1 : BOOST_CHECK_EQUAL(snapshot3.mnSkipListMode, skipMode);
48 1 : BOOST_CHECK(snapshot3.mnSkipList == skipList);
49 1 : }
50 :
51 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_serialization_test)
52 : {
53 : // Test with various configurations
54 1 : std::vector<bool> activeMembers = CreateBitVector(10, {0, 2, 4, 6, 8});
55 1 : auto skipMode = SnapshotSkipMode::MODE_SKIPPING_ENTRIES;
56 1 : std::vector<int> skipList = {10, 20, 30};
57 :
58 1 : CQuorumSnapshot snapshot(activeMembers, skipMode, skipList);
59 :
60 : // Test serialization roundtrip
61 1 : CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
62 1 : ss << snapshot;
63 :
64 1 : CQuorumSnapshot deserialized;
65 1 : ss >> deserialized;
66 :
67 1 : BOOST_CHECK(deserialized.activeQuorumMembers == snapshot.activeQuorumMembers);
68 1 : BOOST_CHECK_EQUAL(deserialized.mnSkipListMode, snapshot.mnSkipListMode);
69 1 : BOOST_CHECK(deserialized.mnSkipList == snapshot.mnSkipList);
70 1 : }
71 :
72 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_skip_modes_test)
73 : {
74 : // Test all skip modes
75 1 : constexpr std::array<SnapshotSkipMode, 4> skipModes{
76 : SnapshotSkipMode::MODE_NO_SKIPPING,
77 : SnapshotSkipMode::MODE_SKIPPING_ENTRIES,
78 : SnapshotSkipMode::MODE_NO_SKIPPING_ENTRIES,
79 : SnapshotSkipMode::MODE_ALL_SKIPPED
80 : };
81 :
82 5 : for (auto mode : skipModes) {
83 4 : CQuorumSnapshot snapshot({true, false, true}, mode, {1, 2, 3});
84 :
85 4 : CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
86 4 : ss << snapshot;
87 :
88 4 : CQuorumSnapshot deserialized;
89 4 : ss >> deserialized;
90 :
91 4 : BOOST_CHECK_EQUAL(deserialized.mnSkipListMode, mode);
92 4 : }
93 1 : }
94 :
95 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_large_data_test)
96 : {
97 : // Test with large quorum (400 members)
98 1 : std::vector<bool> largeActiveMembers(400);
99 : // Create pattern: every 3rd member is inactive
100 401 : for (size_t i = 0; i < largeActiveMembers.size(); i++) {
101 400 : largeActiveMembers[i] = (i % 3 != 0);
102 400 : }
103 :
104 : // Create large skip list
105 1 : std::vector<int> largeSkipList;
106 101 : for (int i = 0; i < 100; i++) {
107 100 : largeSkipList.push_back(i * 4);
108 100 : }
109 :
110 1 : CQuorumSnapshot snapshot(largeActiveMembers, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, largeSkipList);
111 :
112 : // Test serialization with large data
113 : // Test serialization manually instead of using roundtrip helper
114 1 : CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
115 1 : ss << snapshot;
116 1 : CQuorumSnapshot deserialized;
117 1 : ss >> deserialized;
118 1 : BOOST_CHECK_EQUAL(deserialized.activeQuorumMembers.size(), 400);
119 1 : BOOST_CHECK_EQUAL(deserialized.mnSkipList.size(), 100);
120 1 : }
121 :
122 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_empty_data_test)
123 : {
124 : // Test with empty data
125 1 : CQuorumSnapshot emptySnapshot({}, SnapshotSkipMode::MODE_NO_SKIPPING, {});
126 :
127 : // Test serialization roundtrip
128 1 : BOOST_CHECK(TestSerializationRoundtrip(emptySnapshot));
129 :
130 : // Test with empty active members but non-empty skip list
131 1 : CQuorumSnapshot snapshot1({}, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, {1, 2, 3});
132 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshot1));
133 :
134 : // Test with non-empty active members but empty skip list
135 1 : CQuorumSnapshot snapshot2({true, false, true}, SnapshotSkipMode::MODE_NO_SKIPPING, {});
136 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshot2));
137 1 : }
138 :
139 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_bit_serialization_test)
140 : {
141 : // Test bit vector serialization edge cases
142 :
143 : // Test single bit
144 1 : CQuorumSnapshot snapshot1({true}, SnapshotSkipMode::MODE_NO_SKIPPING, {});
145 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshot1));
146 :
147 : // Test 8 bits (full byte)
148 1 : CQuorumSnapshot snapshot8(std::vector<bool>(8, true), SnapshotSkipMode::MODE_NO_SKIPPING, {});
149 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshot8));
150 :
151 : // Test 9 bits (more than one byte)
152 1 : CQuorumSnapshot snapshot9(std::vector<bool>(9, false), SnapshotSkipMode::MODE_NO_SKIPPING, {});
153 1 : snapshot9.activeQuorumMembers[8] = true; // Set last bit
154 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshot9));
155 :
156 : // Test alternating pattern
157 1 : std::vector<bool> alternating(16);
158 17 : for (size_t i = 0; i < alternating.size(); i++) {
159 16 : alternating[i] = (i % 2 == 0);
160 16 : }
161 1 : CQuorumSnapshot snapshotAlt(alternating, SnapshotSkipMode::MODE_NO_SKIPPING, {});
162 1 : BOOST_CHECK(TestSerializationRoundtrip(snapshotAlt));
163 1 : }
164 :
165 149 : BOOST_AUTO_TEST_CASE(quorum_rotation_info_construction_test)
166 : {
167 1 : CQuorumRotationInfo rotInfo;
168 :
169 : // Test default state
170 1 : BOOST_CHECK(!rotInfo.extraShare);
171 1 : BOOST_CHECK(!rotInfo.cycleHMinus4C.has_value());
172 1 : BOOST_CHECK(rotInfo.lastCommitmentPerIndex.empty());
173 1 : BOOST_CHECK(rotInfo.quorumSnapshotList.empty());
174 1 : BOOST_CHECK(rotInfo.mnListDiffList.empty());
175 1 : }
176 :
177 : // Note: CQuorumRotationInfo serialization requires complex setup
178 : // This is better tested in functional tests
179 :
180 149 : BOOST_AUTO_TEST_CASE(get_last_base_block_hash_repeated_base_blocks_test)
181 : {
182 1 : std::vector<CBlockIndex> blocks(4);
183 1 : std::vector<uint256> hashes{
184 1 : GetTestBlockHash(10),
185 1 : GetTestBlockHash(20),
186 1 : GetTestBlockHash(30),
187 1 : GetTestBlockHash(40),
188 : };
189 5 : for (size_t i{0}; i < blocks.size(); ++i) {
190 4 : blocks[i].nHeight = static_cast<int>((i + 1) * 10);
191 4 : blocks[i].phashBlock = &hashes[i];
192 4 : }
193 :
194 : // Non-legacy: sorts internally, so unsorted input with duplicates is fine.
195 4 : std::vector<const CBlockIndex*> unsorted_repeated_base_blocks{
196 1 : &blocks[2],
197 1 : &blocks[0],
198 1 : &blocks[1],
199 1 : &blocks[1],
200 : };
201 1 : BOOST_CHECK(GetLastBaseBlockHash(unsorted_repeated_base_blocks, &blocks[3], false) == hashes[2]);
202 1 : BOOST_CHECK(GetLastBaseBlockHash(unsorted_repeated_base_blocks, &blocks[1], false) == hashes[1]);
203 :
204 : // Legacy: relies on caller-supplied sort and tolerates duplicates as a no-op.
205 : // BuildQuorumRotationInfo deliberately does NOT deduplicate in the legacy path so
206 : // the wire response to older peers stays bit-for-bit identical; these checks
207 : // demonstrate that the duplicate is harmless to GetLastBaseBlockHash's output.
208 4 : std::vector<const CBlockIndex*> sorted_repeated_base_blocks{
209 1 : &blocks[0],
210 1 : &blocks[1],
211 1 : &blocks[1],
212 1 : &blocks[2],
213 : };
214 3 : std::vector<const CBlockIndex*> sorted_unique_base_blocks{
215 1 : &blocks[0],
216 1 : &blocks[1],
217 1 : &blocks[2],
218 : };
219 1 : BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[3], true) == hashes[2]);
220 1 : BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[1], true) == hashes[1]);
221 : // Legacy no-op proof: duplicate vs unique input produces the same hash.
222 1 : BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[3], true) ==
223 : GetLastBaseBlockHash(sorted_unique_base_blocks, &blocks[3], true));
224 1 : BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[1], true) ==
225 : GetLastBaseBlockHash(sorted_unique_base_blocks, &blocks[1], true));
226 1 : }
227 :
228 149 : BOOST_AUTO_TEST_CASE(get_quorum_rotation_info_serialization_test)
229 : {
230 1 : CGetQuorumRotationInfo getInfo;
231 :
232 : // Test with multiple base block hashes
233 1 : getInfo.baseBlockHashes = {GetTestBlockHash(1), GetTestBlockHash(2), GetTestBlockHash(3)};
234 1 : getInfo.blockRequestHash = GetTestBlockHash(100);
235 1 : getInfo.extraShare = true;
236 :
237 : // Test serialization
238 1 : BOOST_CHECK(TestSerializationRoundtrip(getInfo));
239 :
240 : // Test with empty base block hashes
241 1 : CGetQuorumRotationInfo emptyInfo;
242 1 : emptyInfo.blockRequestHash = GetTestBlockHash(200);
243 1 : emptyInfo.extraShare = false;
244 :
245 1 : BOOST_CHECK(TestSerializationRoundtrip(emptyInfo));
246 1 : }
247 :
248 149 : BOOST_AUTO_TEST_CASE(quorum_rotation_info_serialization_test)
249 : {
250 : // Note: mnListDiff{smth} testing requires proper CSimplifiedMNListDiff setup
251 : // which is complex and better tested in functional tests
252 :
253 : // Test CQuorumRotationInfo serialization with various optional field combinations
254 1 : CQuorumRotationInfo rotInfo;
255 :
256 : // Set up basic required fields
257 1 : rotInfo.cycleHMinusC.m_snap = CQuorumSnapshot({true, false, true}, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, {1, 2});
258 1 : rotInfo.cycleHMinus2C.m_snap = CQuorumSnapshot({false, true, false}, SnapshotSkipMode::MODE_NO_SKIPPING, {});
259 1 : rotInfo.cycleHMinus3C.m_snap = CQuorumSnapshot({true, true, false}, SnapshotSkipMode::MODE_ALL_SKIPPED, {3});
260 :
261 : // Test without extraShare
262 1 : rotInfo.extraShare = false;
263 1 : BOOST_CHECK(TestSerializationRoundtrip(rotInfo));
264 :
265 : // Test with extraShare but uninitialized optional fields
266 1 : rotInfo.extraShare = true;
267 1 : BOOST_CHECK(TestSerializationRoundtrip(rotInfo));
268 :
269 : // Test with extraShare and initialized snapshot
270 1 : llmq::CycleData extra_cycle;
271 1 : extra_cycle.m_snap = CQuorumSnapshot({false, false, true}, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, {4, 5, 6});
272 1 : rotInfo.cycleHMinus4C = extra_cycle;
273 1 : BOOST_CHECK(TestSerializationRoundtrip(rotInfo));
274 :
275 1 : CFinalCommitment commitment{GetLLMQParams(Consensus::LLMQType::LLMQ_TEST), uint256::ONE};
276 1 : rotInfo.lastCommitmentPerIndex.push_back(commitment);
277 2 : rotInfo.quorumSnapshotList.push_back(
278 1 : CQuorumSnapshot({false, false, true}, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, {7, 8}));
279 1 : BOOST_CHECK(TestSerializationRoundtrip(rotInfo));
280 1 : }
281 :
282 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_json_test)
283 : {
284 : // Create snapshot with test data
285 1 : std::vector<bool> activeMembers = {true, false, true, true, false, false, true};
286 1 : auto skipMode = SnapshotSkipMode::MODE_SKIPPING_ENTRIES;
287 1 : std::vector<int> skipList = {10, 20, 30, 40};
288 :
289 1 : CQuorumSnapshot snapshot(activeMembers, skipMode, skipList);
290 :
291 : // Test JSON conversion
292 1 : UniValue json = snapshot.ToJson();
293 :
294 : // Verify JSON structure
295 1 : BOOST_CHECK(json.isObject());
296 1 : BOOST_CHECK(json.exists("activeQuorumMembers"));
297 1 : BOOST_CHECK(json.exists("mnSkipListMode"));
298 1 : BOOST_CHECK(json.exists("mnSkipList"));
299 :
300 : // Verify skip list is array
301 1 : BOOST_CHECK(json["mnSkipList"].isArray());
302 1 : BOOST_CHECK_EQUAL(json["mnSkipList"].size(), skipList.size());
303 1 : }
304 :
305 149 : BOOST_AUTO_TEST_CASE(quorum_snapshot_malformed_data_test)
306 : {
307 : // Create valid snapshot
308 1 : CQuorumSnapshot snapshot({true, false, true}, SnapshotSkipMode::MODE_SKIPPING_ENTRIES, {1, 2, 3});
309 :
310 1 : CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
311 1 : ss << snapshot;
312 :
313 : // Test truncated data deserialization
314 1 : std::string data = ss.str();
315 5 : for (size_t truncateAt = 1; truncateAt < data.size(); truncateAt += 5) {
316 4 : CDataStream truncated(std::vector<unsigned char>(data.begin(), data.begin() + truncateAt), SER_NETWORK,
317 : PROTOCOL_VERSION);
318 :
319 4 : CQuorumSnapshot deserialized;
320 : try {
321 4 : truncated >> deserialized;
322 : // If no exception, it might be a valid partial deserialization
323 : // (though unlikely for complex structures)
324 4 : } catch (const std::exception&) {
325 : // Expected for most truncation points
326 4 : }
327 4 : }
328 5 : }
329 :
330 146 : BOOST_AUTO_TEST_SUITE_END()
|