diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index 81043f98729d..389b624c01fd 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -14,6 +14,8 @@ #include +#include + namespace { constexpr std::string_view DB_QUORUM_SNAPSHOT{"llmq_S"}; @@ -74,9 +76,15 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan } baseBlockIndexes.push_back(blockIndex); } - if (use_legacy_construction) { - std::sort(baseBlockIndexes.begin(), baseBlockIndexes.end(), - [](const CBlockIndex* a, const CBlockIndex* b) { return a->nHeight < b->nHeight; }); + // Sort in all cases: the legacy path (served to peers < EFFICIENT_QRINFO_VERSION) + // relies on the order for baseBlockIndexes.back() and GetLastBaseBlockHash(). + std::sort(baseBlockIndexes.begin(), baseBlockIndexes.end(), + [](const CBlockIndex* a, const CBlockIndex* b) { return a->nHeight < b->nHeight; }); + if (!use_legacy_construction) { + // Only deduplicate on the non-legacy path; leave the legacy path untouched so the + // wire response to older peers stays bit-for-bit identical to the pre-fix behavior. + baseBlockIndexes.erase(std::unique(baseBlockIndexes.begin(), baseBlockIndexes.end()), + baseBlockIndexes.end()); } } diff --git a/src/test/llmq_snapshot_tests.cpp b/src/test/llmq_snapshot_tests.cpp index 7e3b1d191ece..8ed6c16da5d2 100644 --- a/src/test/llmq_snapshot_tests.cpp +++ b/src/test/llmq_snapshot_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -176,6 +177,54 @@ BOOST_AUTO_TEST_CASE(quorum_rotation_info_construction_test) // Note: CQuorumRotationInfo serialization requires complex setup // This is better tested in functional tests +BOOST_AUTO_TEST_CASE(get_last_base_block_hash_repeated_base_blocks_test) +{ + std::vector blocks(4); + std::vector hashes{ + GetTestBlockHash(10), + GetTestBlockHash(20), + GetTestBlockHash(30), + GetTestBlockHash(40), + }; + for (size_t i{0}; i < blocks.size(); ++i) { + blocks[i].nHeight = static_cast((i + 1) * 10); + blocks[i].phashBlock = &hashes[i]; + } + + // Non-legacy: sorts internally, so unsorted input with duplicates is fine. + std::vector unsorted_repeated_base_blocks{ + &blocks[2], + &blocks[0], + &blocks[1], + &blocks[1], + }; + BOOST_CHECK(GetLastBaseBlockHash(unsorted_repeated_base_blocks, &blocks[3], false) == hashes[2]); + BOOST_CHECK(GetLastBaseBlockHash(unsorted_repeated_base_blocks, &blocks[1], false) == hashes[1]); + + // Legacy: relies on caller-supplied sort and tolerates duplicates as a no-op. + // BuildQuorumRotationInfo deliberately does NOT deduplicate in the legacy path so + // the wire response to older peers stays bit-for-bit identical; these checks + // demonstrate that the duplicate is harmless to GetLastBaseBlockHash's output. + std::vector sorted_repeated_base_blocks{ + &blocks[0], + &blocks[1], + &blocks[1], + &blocks[2], + }; + std::vector sorted_unique_base_blocks{ + &blocks[0], + &blocks[1], + &blocks[2], + }; + BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[3], true) == hashes[2]); + BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[1], true) == hashes[1]); + // Legacy no-op proof: duplicate vs unique input produces the same hash. + BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[3], true) == + GetLastBaseBlockHash(sorted_unique_base_blocks, &blocks[3], true)); + BOOST_CHECK(GetLastBaseBlockHash(sorted_repeated_base_blocks, &blocks[1], true) == + GetLastBaseBlockHash(sorted_unique_base_blocks, &blocks[1], true)); +} + BOOST_AUTO_TEST_CASE(get_quorum_rotation_info_serialization_test) { CGetQuorumRotationInfo getInfo; diff --git a/test/functional/feature_llmq_rotation.py b/test/functional/feature_llmq_rotation.py index f0e3651ddbf5..813016c5b9ef 100755 --- a/test/functional/feature_llmq_rotation.py +++ b/test/functional/feature_llmq_rotation.py @@ -231,6 +231,9 @@ def run_test(self): hmc_base_blockhash = self.nodes[0].getblockhash(block_count - (block_count % 24) - 24 - 8) best_block_hash = self.nodes[0].getbestblockhash() rpc_qr_info = self.nodes[0].quorum("rotationinfo", best_block_hash, False, [hmc_base_blockhash]) + rpc_qr_info_repeated_base = self.nodes[0].quorum("rotationinfo", best_block_hash, False, + [hmc_base_blockhash, hmc_base_blockhash]) + assert_equal(rpc_qr_info_repeated_base, rpc_qr_info) assert_equal(rpc_qr_info["mnListDiffTip"]["blockHash"], best_block_hash) assert_equal(rpc_qr_info["mnListDiffTip"]["baseBlockHash"], rpc_qr_info["mnListDiffH"]["blockHash"]) assert_equal(rpc_qr_info["mnListDiffH"]["baseBlockHash"], rpc_qr_info["mnListDiffAtHMinusC"]["blockHash"])