Skip to content

test: add comprehensive unit tests for LLMQ subsystem #6691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
6 changes: 6 additions & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ BITCOIN_TESTS =\
test/lcg.h \
test/limitedmap_tests.cpp \
test/llmq_dkg_tests.cpp \
test/llmq_chainlock_tests.cpp \
test/llmq_commitment_tests.cpp \
test/llmq_hash_tests.cpp \
test/llmq_params_tests.cpp \
test/llmq_snapshot_tests.cpp \
test/llmq_utils_tests.cpp \
test/logging_tests.cpp \
test/dbwrapper_tests.cpp \
test/validation_tests.cpp \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test_util.include
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ TEST_UTIL_H = \
test/util/chainstate.h \
test/util/json.h \
test/util/index.h \
test/util/llmq_tests.h \
test/util/logging.h \
test/util/mining.h \
test/util/net.h \
Expand Down
168 changes: 168 additions & 0 deletions src/test/llmq_chainlock_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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 <test/util/llmq_tests.h>
#include <test/util/setup_common.h>

#include <llmq/clsig.h>
#include <streams.h>
#include <util/strencodings.h>

#include <boost/test/unit_test.hpp>

using namespace llmq;
using namespace llmq::testutils;

BOOST_FIXTURE_TEST_SUITE(llmq_chainlock_tests, BasicTestingSetup)

BOOST_AUTO_TEST_CASE(chainlock_construction_test)
{
// Test default constructor
CChainLockSig clsig1;
BOOST_CHECK(clsig1.IsNull());
BOOST_CHECK_EQUAL(clsig1.getHeight(), -1);
BOOST_CHECK(clsig1.getBlockHash().IsNull());
BOOST_CHECK(!clsig1.getSig().IsValid());

// Test parameterized constructor
int32_t height = 12345;
uint256 blockHash = GetTestBlockHash(1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: names should be named in snake_case for new code; for example: block_hash instead blockHash.
nullClsig -> null_clsig (or clsig_null)
nonRotatedParams -> params_non_rotated (or non_rotated_params)
truncateAt -> truncate_at
hashDiffQuorum -> hash_diff_quorum
hashDiffPubKey -> hash_diff_pubkey
hashDiffVvec -> hash_diff_vvec
hashEmpty -> hash_empty
hashNoneValid -> hash_none_valid
hashAllValid -> hash_all_valid
proTxHash1 -> protx_hash1

also, hashNullQuorum, hashInvalidKey, hashNullVvec, largeValidMembers, hashLarge, hashLargeDiff, nonRotated, getInfo, activeMembers, skipMode, skipList, nullHash, validHash

See doc/developer-notes.md:

- **Symbol naming conventions**. These are preferred in new code, but are not
required when doing so would need changes to significant pieces of existing
code.
  - Variable (including function arguments) and namespace names are all lowercase and may use `_` to
    separate words (snake_case).
    - Class member variables have a `m_` prefix.
    - Global variables have a `g_` prefix.
  - Constant names are all uppercase, and use `_` to separate words.
  - Enumerator constants may be `snake_case`, `PascalCase` or `ALL_CAPS`.
    This is a more tolerant policy than the [C++ Core
    Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Renum-caps),
    which recommend using `snake_case`.  Please use what seems appropriate.
  - Class names, function names, and method names are UpperCamelCase
    (PascalCase). Do not prefix class names with `C`. See [Internal interface
    naming style](#internal-interface-naming-style) for an exception to this
    convention.

CBLSSignature sig = CreateRandomBLSSignature();

CChainLockSig clsig2(height, blockHash, sig);
BOOST_CHECK(!clsig2.IsNull());
BOOST_CHECK_EQUAL(clsig2.getHeight(), height);
BOOST_CHECK(clsig2.getBlockHash() == blockHash);
BOOST_CHECK(clsig2.getSig() == sig);
}

BOOST_AUTO_TEST_CASE(chainlock_null_test)
{
CChainLockSig clsig;

// Default constructed should be null
BOOST_CHECK(clsig.IsNull());

// With height set but null hash, should not be null
clsig = CChainLockSig(100, uint256(), CBLSSignature());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit use {}:

    clsig = CChainLockSig(100, uint256{}, CBLSSignature{});

BOOST_CHECK(!clsig.IsNull());

// With valid data should not be null
clsig = CreateChainLock(100, GetTestBlockHash(1));
BOOST_CHECK(!clsig.IsNull());
}

BOOST_AUTO_TEST_CASE(chainlock_serialization_test)
{
// Test with valid chainlock
CChainLockSig clsig = CreateChainLock(67890, GetTestBlockHash(42));

// Test serialization preserves all fields
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << clsig;

CChainLockSig deserialized;
ss >> deserialized;

BOOST_CHECK_EQUAL(clsig.getHeight(), deserialized.getHeight());
BOOST_CHECK(clsig.getBlockHash() == deserialized.getBlockHash());
BOOST_CHECK(clsig.getSig() == deserialized.getSig());
BOOST_CHECK_EQUAL(clsig.IsNull(), deserialized.IsNull());
}

BOOST_AUTO_TEST_CASE(chainlock_tostring_test)
{
// Test null chainlock
CChainLockSig nullClsig;
std::string nullStr = nullClsig.ToString();
BOOST_CHECK(!nullStr.empty());

// Test valid chainlock
int32_t height = 123456;
uint256 blockHash = GetTestBlockHash(789);
CChainLockSig clsig = CreateChainLock(height, blockHash);

std::string str = clsig.ToString();
BOOST_CHECK(!str.empty());

// ToString should contain height and hash info
BOOST_CHECK(str.find(strprintf("%d", height)) != std::string::npos);
BOOST_CHECK(str.find(blockHash.ToString().substr(0, 10)) != std::string::npos);
}

BOOST_AUTO_TEST_CASE(chainlock_edge_cases_test)
{
// Test with edge case heights
CChainLockSig clsig1 = CreateChainLock(0, GetTestBlockHash(1));
BOOST_CHECK_EQUAL(clsig1.getHeight(), 0);
BOOST_CHECK(!clsig1.IsNull());

CChainLockSig clsig2 = CreateChainLock(std::numeric_limits<int32_t>::max(), GetTestBlockHash(2));
BOOST_CHECK_EQUAL(clsig2.getHeight(), std::numeric_limits<int32_t>::max());

// Test serialization with extreme values
CDataStream ss1(SER_NETWORK, PROTOCOL_VERSION);
ss1 << clsig1;
CChainLockSig clsig1_deserialized;
ss1 >> clsig1_deserialized;
BOOST_CHECK_EQUAL(clsig1.getHeight(), clsig1_deserialized.getHeight());

CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION);
ss2 << clsig2;
CChainLockSig clsig2_deserialized;
ss2 >> clsig2_deserialized;
BOOST_CHECK_EQUAL(clsig2.getHeight(), clsig2_deserialized.getHeight());
}

BOOST_AUTO_TEST_CASE(chainlock_comparison_test)
{
// Create identical chainlocks
int32_t height = 5000;
uint256 blockHash = GetTestBlockHash(10);
CBLSSignature sig = CreateRandomBLSSignature();

CChainLockSig clsig1(height, blockHash, sig);
CChainLockSig clsig2(height, blockHash, sig);

// Verify getters return same values
BOOST_CHECK_EQUAL(clsig1.getHeight(), clsig2.getHeight());
BOOST_CHECK(clsig1.getBlockHash() == clsig2.getBlockHash());
BOOST_CHECK(clsig1.getSig() == clsig2.getSig());

// Different chainlocks
CChainLockSig clsig3(height + 1, blockHash, sig);
BOOST_CHECK(clsig1.getHeight() != clsig3.getHeight());

CChainLockSig clsig4(height, GetTestBlockHash(11), sig);
BOOST_CHECK(clsig1.getBlockHash() != clsig4.getBlockHash());
}

BOOST_AUTO_TEST_CASE(chainlock_malformed_data_test)
{
// Test deserialization of truncated data
CChainLockSig clsig = CreateChainLock(1000, GetTestBlockHash(5));

CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << clsig;

// Truncate the stream
std::string data = ss.str();
for (size_t truncateAt = 1; truncateAt < data.size(); truncateAt += 10) {
CDataStream truncated(std::vector<unsigned char>(data.begin(), data.begin() + truncateAt), SER_NETWORK,
PROTOCOL_VERSION);

CChainLockSig deserialized;
try {
truncated >> deserialized;
// If no exception, verify it's either complete or default
if (truncateAt < sizeof(int32_t)) {
BOOST_CHECK(deserialized.IsNull());
}
} catch (const std::exception&) {
// Expected for most truncation points
}
}
}

BOOST_AUTO_TEST_SUITE_END()
Loading
Loading