Skip to content

Commit 99df68b

Browse files
test: add unit tests for LLMQ signing shares validation
1 parent f55fd90 commit 99df68b

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ BITCOIN_TESTS =\
137137
test/llmq_commitment_tests.cpp \
138138
test/llmq_hash_tests.cpp \
139139
test/llmq_params_tests.cpp \
140+
test/llmq_signing_shares_tests.cpp \
140141
test/llmq_snapshot_tests.cpp \
141142
test/llmq_utils_tests.cpp \
142143
test/logging_tests.cpp \
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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/setup_common.h>
6+
#include <test/util/llmq_tests.h>
7+
8+
#include <bls/bls.h>
9+
#include <chainparams.h>
10+
#include <evo/deterministicmns.h>
11+
#include <llmq/commitment.h>
12+
#include <llmq/params.h>
13+
#include <llmq/quorums.h>
14+
#include <llmq/signing.h>
15+
#include <llmq/signing_shares.h>
16+
#include <masternode/node.h>
17+
18+
#include <boost/test/unit_test.hpp>
19+
20+
using namespace llmq;
21+
using namespace llmq::testutils;
22+
23+
// Test fixture with helper functions
24+
struct LLMQSigningSharesTestFixture : public TestingSetup
25+
{
26+
std::unique_ptr<CBLSWorker> blsWorker;
27+
28+
LLMQSigningSharesTestFixture() : TestingSetup()
29+
{
30+
blsWorker = std::make_unique<CBLSWorker>();
31+
}
32+
33+
// Helper to create a minimal test quorum
34+
CQuorumCPtr CreateMinimalTestQuorum(int size, bool hasVerificationVector = true,
35+
const std::vector<bool>& validMembers = {})
36+
{
37+
const auto& params = GetLLMQParams(Consensus::LLMQType::LLMQ_TEST_V17);
38+
39+
auto quorum = std::make_shared<CQuorum>(params, *blsWorker);
40+
41+
// Create commitment
42+
auto qc_ptr = std::make_unique<CFinalCommitment>();
43+
qc_ptr->llmqType = params.type;
44+
qc_ptr->quorumHash = InsecureRand256();
45+
46+
// Set valid members
47+
if (!validMembers.empty()) {
48+
qc_ptr->validMembers = validMembers;
49+
} else {
50+
qc_ptr->validMembers.resize(size, true);
51+
}
52+
53+
// Create members (empty DMN pointers are fine for our tests)
54+
std::vector<CDeterministicMNCPtr> members(size, nullptr);
55+
56+
quorum->Init(std::move(qc_ptr), nullptr, InsecureRand256(), members);
57+
58+
// Set verification vector if requested
59+
if (hasVerificationVector) {
60+
std::vector<CBLSPublicKey> vvec;
61+
for (int i = 0; i < size; ++i) {
62+
CBLSSecretKey sk;
63+
sk.MakeNewKey();
64+
vvec.push_back(sk.GetPublicKey());
65+
}
66+
quorum->SetVerificationVector(vvec);
67+
}
68+
69+
return quorum;
70+
}
71+
72+
// Helper to create test SessionInfo
73+
CSigSharesNodeState::SessionInfo CreateTestSessionInfo(CQuorumCPtr quorum)
74+
{
75+
CSigSharesNodeState::SessionInfo session;
76+
session.llmqType = quorum->params.type;
77+
session.quorumHash = quorum->qc->quorumHash;
78+
session.id = InsecureRand256();
79+
session.msgHash = InsecureRand256();
80+
session.quorum = quorum;
81+
return session;
82+
}
83+
84+
// Helper to create test BatchedSigShares
85+
CBatchedSigShares CreateTestBatchedSigShares(const std::vector<uint16_t>& members)
86+
{
87+
CBatchedSigShares batched;
88+
batched.sessionId = 1;
89+
90+
for (uint16_t member : members) {
91+
CBLSLazySignature lazySig;
92+
batched.sigShares.emplace_back(member, lazySig);
93+
}
94+
95+
return batched;
96+
}
97+
};
98+
99+
BOOST_FIXTURE_TEST_SUITE(llmq_signing_shares_tests, LLMQSigningSharesTestFixture)
100+
101+
// Test: Missing verification vector
102+
BOOST_AUTO_TEST_CASE(preverify_missing_verification_vector)
103+
{
104+
// Create quorum WITHOUT verification vector
105+
auto quorum = CreateMinimalTestQuorum(3, false);
106+
auto sessionInfo = CreateTestSessionInfo(quorum);
107+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1});
108+
109+
// Note: We can't easily test the full function because IsQuorumActive and IsMember
110+
// require complex setup. This test verifies the data structures are created correctly.
111+
// The missing verification vector check will be hit if the earlier checks pass.
112+
113+
BOOST_CHECK(!quorum->HasVerificationVector());
114+
BOOST_CHECK_EQUAL(quorum->members.size(), 3);
115+
}
116+
117+
// Test: Duplicate member detection
118+
BOOST_AUTO_TEST_CASE(preverify_duplicate_member)
119+
{
120+
// Create a valid quorum
121+
auto quorum = CreateMinimalTestQuorum(5, true);
122+
auto sessionInfo = CreateTestSessionInfo(quorum);
123+
124+
// Create batch with duplicate member (0 appears twice)
125+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 0, 2});
126+
127+
// We can test the duplicate detection logic by checking the batch structure
128+
std::unordered_set<uint16_t> seen;
129+
bool hasDuplicate = false;
130+
for (const auto& [member, _] : batchedSigShares.sigShares) {
131+
if (!seen.insert(member).second) {
132+
hasDuplicate = true;
133+
break;
134+
}
135+
}
136+
137+
BOOST_CHECK(hasDuplicate);
138+
}
139+
140+
// Test: Quorum member out of bounds
141+
BOOST_AUTO_TEST_CASE(preverify_member_out_of_bounds)
142+
{
143+
// Create quorum with 5 members
144+
auto quorum = CreateMinimalTestQuorum(5, true);
145+
auto sessionInfo = CreateTestSessionInfo(quorum);
146+
147+
// Create batch with member index out of bounds (>= 5)
148+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 10});
149+
150+
// Verify that we have an out-of-bounds member
151+
bool hasOutOfBounds = false;
152+
for (const auto& [member, _] : batchedSigShares.sigShares) {
153+
if (member >= quorum->members.size()) {
154+
hasOutOfBounds = true;
155+
break;
156+
}
157+
}
158+
159+
BOOST_CHECK(hasOutOfBounds);
160+
BOOST_CHECK_EQUAL(quorum->members.size(), 5);
161+
}
162+
163+
// Test: Invalid quorum member
164+
BOOST_AUTO_TEST_CASE(preverify_invalid_quorum_member)
165+
{
166+
// Create quorum with specific valid members pattern
167+
std::vector<bool> validMembers = {true, false, true, true, false};
168+
auto quorum = CreateMinimalTestQuorum(5, true, validMembers);
169+
auto sessionInfo = CreateTestSessionInfo(quorum);
170+
171+
// Create batch including an invalid member (member 1 is invalid)
172+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2});
173+
174+
// Verify that member 1 is marked invalid
175+
BOOST_CHECK_EQUAL(quorum->qc->validMembers[0], true);
176+
BOOST_CHECK_EQUAL(quorum->qc->validMembers[1], false); // Invalid!
177+
BOOST_CHECK_EQUAL(quorum->qc->validMembers[2], true);
178+
179+
// Check that we can detect the invalid member
180+
bool hasInvalidMember = false;
181+
for (const auto& [member, _] : batchedSigShares.sigShares) {
182+
if (member < quorum->qc->validMembers.size() &&
183+
!quorum->qc->validMembers[member]) {
184+
hasInvalidMember = true;
185+
break;
186+
}
187+
}
188+
189+
BOOST_CHECK(hasInvalidMember);
190+
}
191+
192+
// Test: Valid batch structure
193+
BOOST_AUTO_TEST_CASE(preverify_valid_batch_structure)
194+
{
195+
// Create a valid quorum
196+
auto quorum = CreateMinimalTestQuorum(5, true);
197+
auto sessionInfo = CreateTestSessionInfo(quorum);
198+
199+
// Create a valid batch (all members exist and are unique)
200+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2, 3, 4});
201+
202+
// Verify no duplicates
203+
std::unordered_set<uint16_t> seen;
204+
bool hasDuplicate = false;
205+
for (const auto& [member, _] : batchedSigShares.sigShares) {
206+
if (!seen.insert(member).second) {
207+
hasDuplicate = true;
208+
break;
209+
}
210+
}
211+
BOOST_CHECK(!hasDuplicate);
212+
213+
// Verify all members are in bounds
214+
bool allInBounds = true;
215+
for (const auto& [member, _] : batchedSigShares.sigShares) {
216+
if (member >= quorum->members.size()) {
217+
allInBounds = false;
218+
break;
219+
}
220+
}
221+
BOOST_CHECK(allInBounds);
222+
223+
// Verify all members are valid
224+
bool allValid = true;
225+
for (const auto& [member, _] : batchedSigShares.sigShares) {
226+
if (member >= quorum->qc->validMembers.size() ||
227+
!quorum->qc->validMembers[member]) {
228+
allValid = false;
229+
break;
230+
}
231+
}
232+
BOOST_CHECK(allValid);
233+
}
234+
235+
// Test: Empty batch
236+
BOOST_AUTO_TEST_CASE(preverify_empty_batch)
237+
{
238+
// Create a valid quorum
239+
auto quorum = CreateMinimalTestQuorum(5, true);
240+
auto sessionInfo = CreateTestSessionInfo(quorum);
241+
242+
// Create an empty batch
243+
auto batchedSigShares = CreateTestBatchedSigShares({});
244+
245+
// Empty batch should have no shares
246+
BOOST_CHECK(batchedSigShares.sigShares.empty());
247+
248+
// Empty batch should pass all validation checks (nothing to validate)
249+
for (const auto& share : batchedSigShares.sigShares) {
250+
// This loop shouldn't execute
251+
(void)share; // Suppress unused variable warning
252+
BOOST_CHECK(false);
253+
}
254+
}
255+
256+
// Test: Multiple duplicates
257+
BOOST_AUTO_TEST_CASE(preverify_multiple_duplicates)
258+
{
259+
auto quorum = CreateMinimalTestQuorum(10, true);
260+
auto sessionInfo = CreateTestSessionInfo(quorum);
261+
262+
// Create batch with multiple duplicates
263+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2, 1, 3, 2, 4});
264+
265+
// Count duplicates
266+
std::unordered_set<uint16_t> seen;
267+
int duplicateCount = 0;
268+
for (const auto& [member, _] : batchedSigShares.sigShares) {
269+
if (!seen.insert(member).second) {
270+
duplicateCount++;
271+
}
272+
}
273+
274+
BOOST_CHECK_EQUAL(duplicateCount, 2); // Members 1 and 2 appear twice each
275+
}
276+
277+
// Test: Boundary case - maximum member index
278+
BOOST_AUTO_TEST_CASE(preverify_boundary_max_member)
279+
{
280+
const int quorum_size = 10;
281+
auto quorum = CreateMinimalTestQuorum(quorum_size, true);
282+
auto sessionInfo = CreateTestSessionInfo(quorum);
283+
284+
// Create batch with last valid member (size - 1)
285+
auto batchedSigShares = CreateTestBatchedSigShares({0, static_cast<uint16_t>(quorum_size - 1)});
286+
287+
// Verify max member is in bounds
288+
for (const auto& [member, _] : batchedSigShares.sigShares) {
289+
BOOST_CHECK(member < quorum->members.size());
290+
}
291+
}
292+
293+
// Test: All members invalid scenario
294+
BOOST_AUTO_TEST_CASE(preverify_all_members_invalid)
295+
{
296+
// Create quorum where all members are invalid
297+
std::vector<bool> validMembers(5, false);
298+
auto quorum = CreateMinimalTestQuorum(5, true, validMembers);
299+
auto sessionInfo = CreateTestSessionInfo(quorum);
300+
301+
// Create batch with any members
302+
auto batchedSigShares = CreateTestBatchedSigShares({0, 1, 2});
303+
304+
// Verify all members are marked invalid
305+
for (size_t i = 0; i < quorum->qc->validMembers.size(); ++i) {
306+
BOOST_CHECK_EQUAL(quorum->qc->validMembers[i], false);
307+
}
308+
309+
// Check that all shares reference invalid members
310+
bool allInvalid = true;
311+
for (const auto& [member, _] : batchedSigShares.sigShares) {
312+
if (member < quorum->qc->validMembers.size() &&
313+
quorum->qc->validMembers[member]) {
314+
allInvalid = false;
315+
break;
316+
}
317+
}
318+
BOOST_CHECK(allInvalid);
319+
}
320+
321+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)