Skip to content

Commit f26c8be

Browse files
committed
rpc: predict rotated DKG participation
1 parent 9ce1c4d commit f26c8be

4 files changed

Lines changed: 173 additions & 39 deletions

File tree

src/llmq/utils.cpp

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,13 @@ void BuildQuorumSnapshot(const Consensus::LLMQParams& llmqParams, const Consensu
342342
std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQParams& llmqParams,
343343
const llmq::UtilParameters& util_params,
344344
const CDeterministicMNList& allMns,
345-
const PreviousQuorumQuarters& previousQuarters)
345+
const PreviousQuorumQuarters& previousQuarters,
346+
const uint256& modifier,
347+
gsl::not_null<const CBlockIndex*> pDeploymentIndex,
348+
int cycleBaseHeight,
349+
bool storeSnapshot)
346350
{
347-
if (!llmqParams.useRotation || util_params.m_base_index->nHeight % llmqParams.dkgInterval != 0) {
351+
if (!llmqParams.useRotation || cycleBaseHeight % llmqParams.dkgInterval != 0) {
348352
ASSERT_IF_DEBUG(false);
349353
return {};
350354
}
@@ -354,7 +358,6 @@ std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQPar
354358

355359
size_t quorumSize = static_cast<size_t>(llmqParams.size);
356360
auto quarterSize{quorumSize / 4};
357-
const auto modifier = GetHashModifier(llmqParams, util_params.m_chainman.GetConsensus(), util_params.m_base_index);
358361

359362
if (allMns.GetCounts().enabled() < quarterSize) {
360363
return quarterQuorumMembers;
@@ -363,7 +366,7 @@ std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQPar
363366
auto MnsUsedAtH = CDeterministicMNList();
364367
std::vector<CDeterministicMNList> MnsUsedAtHIndexed{nQuorums};
365368

366-
bool skipRemovedMNs = DeploymentActiveAfter(util_params.m_base_index, util_params.m_chainman.GetConsensus(),
369+
bool skipRemovedMNs = DeploymentActiveAfter(pDeploymentIndex.get(), util_params.m_chainman.GetConsensus(),
367370
Consensus::DEPLOYMENT_V19) ||
368371
(util_params.m_chainman.GetParams().NetworkIDString() == CBaseChainParams::TESTNET);
369372

@@ -404,7 +407,7 @@ std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQPar
404407
}
405408

406409
if (LogAcceptDebug(BCLog::LLMQ)) {
407-
LogPrint(BCLog::LLMQ, "%s h[%d] sortedCombinedMns[%s]\n", __func__, util_params.m_base_index->nHeight,
410+
LogPrint(BCLog::LLMQ, "%s h[%d] sortedCombinedMns[%s]\n", __func__, cycleBaseHeight,
408411
ToString(sortedCombinedMnsList));
409412
}
410413

@@ -450,14 +453,31 @@ std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQPar
450453
}
451454
}
452455

453-
llmq::CQuorumSnapshot quorumSnapshot{};
454-
BuildQuorumSnapshot(llmqParams, util_params.m_chainman.GetConsensus(), allMns, MnsUsedAtH, sortedCombinedMnsList,
455-
quorumSnapshot, skipList, util_params.m_base_index);
456-
util_params.m_qsnapman.StoreSnapshotForBlock(llmqParams.type, util_params.m_base_index, quorumSnapshot);
456+
if (storeSnapshot) {
457+
llmq::CQuorumSnapshot quorumSnapshot{};
458+
BuildQuorumSnapshot(llmqParams, util_params.m_chainman.GetConsensus(), allMns, MnsUsedAtH, sortedCombinedMnsList,
459+
quorumSnapshot, skipList, util_params.m_base_index);
460+
util_params.m_qsnapman.StoreSnapshotForBlock(llmqParams.type, util_params.m_base_index, quorumSnapshot);
461+
}
457462

458463
return quarterQuorumMembers;
459464
}
460465

466+
std::vector<QuorumMembers> BuildNewQuorumQuarterMembers(const Consensus::LLMQParams& llmqParams,
467+
const llmq::UtilParameters& util_params,
468+
const CDeterministicMNList& allMns,
469+
const PreviousQuorumQuarters& previousQuarters)
470+
{
471+
if (!llmqParams.useRotation || util_params.m_base_index->nHeight % llmqParams.dkgInterval != 0) {
472+
ASSERT_IF_DEBUG(false);
473+
return {};
474+
}
475+
const auto modifier = GetHashModifier(llmqParams, util_params.m_chainman.GetConsensus(), util_params.m_base_index);
476+
return BuildNewQuorumQuarterMembers(llmqParams, util_params, allMns, previousQuarters, modifier,
477+
util_params.m_base_index, util_params.m_base_index->nHeight,
478+
/*storeSnapshot=*/true);
479+
}
480+
461481
std::vector<QuorumMembers> ComputeQuorumMembersByQuarterRotation(const Consensus::LLMQParams& llmqParams,
462482
const llmq::UtilParameters& util_params)
463483
{
@@ -597,6 +617,85 @@ std::vector<CDeterministicMNCPtr> ComputeNonRotatedQuorumMembersFromWorkBlock(
597617
return CalculateQuorum(mn_list, modifier, llmq_params.size, EvoOnly);
598618
}
599619

620+
std::optional<std::vector<CDeterministicMNCPtr>> ComputeRotatedQuorumMembersFromWorkBlock(
621+
Consensus::LLMQType llmqType, const UtilParameters& util_params,
622+
gsl::not_null<const CBlockIndex*> pWorkBlockIndex, int quorumHeight)
623+
{
624+
const auto& llmq_params_opt = util_params.m_chainman.GetParams().GetLLMQ(llmqType);
625+
if (!llmq_params_opt.has_value()) {
626+
ASSERT_IF_DEBUG(false);
627+
return std::nullopt;
628+
}
629+
const auto& llmq_params = llmq_params_opt.value();
630+
if (!llmq_params.useRotation) {
631+
ASSERT_IF_DEBUG(false);
632+
return std::nullopt;
633+
}
634+
635+
const int quorumIndex{quorumHeight % llmq_params.dkgInterval};
636+
if (quorumIndex < 0 || quorumIndex >= llmq_params.signingActiveQuorumCount) {
637+
ASSERT_IF_DEBUG(false);
638+
return std::nullopt;
639+
}
640+
const int cycleBaseHeight{quorumHeight - quorumIndex};
641+
if (cycleBaseHeight % llmq_params.dkgInterval != 0 ||
642+
pWorkBlockIndex->nHeight != cycleBaseHeight - llmq::WORK_DIFF_DEPTH) {
643+
ASSERT_IF_DEBUG(false);
644+
return std::nullopt;
645+
}
646+
647+
if (!DeploymentActiveAfter(pWorkBlockIndex.get(), util_params.m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) {
648+
// pre-v20 modifier calculation needs the future cycle base block context, which isn't known yet.
649+
return std::nullopt;
650+
}
651+
652+
const auto nQuorums{static_cast<size_t>(llmq_params.signingActiveQuorumCount)};
653+
PreviousQuorumQuarters previousQuarters(nQuorums);
654+
auto prev_cycles{previousQuarters.GetCycles()};
655+
for (size_t idx{0}; idx < prev_cycles.size(); idx++) {
656+
const int previousCycleHeight{cycleBaseHeight - llmq_params.dkgInterval * static_cast<int>(idx + 1)};
657+
if (previousCycleHeight < 0) {
658+
return std::nullopt;
659+
}
660+
prev_cycles[idx]->m_cycle_index = pWorkBlockIndex->GetAncestor(previousCycleHeight);
661+
if (prev_cycles[idx]->m_cycle_index == nullptr) {
662+
return std::nullopt;
663+
}
664+
if (auto opt_snap = util_params.m_qsnapman.GetSnapshotForBlock(llmq_params.type, prev_cycles[idx]->m_cycle_index);
665+
opt_snap.has_value()) {
666+
prev_cycles[idx]->m_snap = opt_snap.value();
667+
} else {
668+
return std::nullopt;
669+
}
670+
prev_cycles[idx]->m_members = GetQuorumQuarterMembersBySnapshot(llmq_params, util_params.m_dmnman,
671+
util_params.m_chainman.GetConsensus(),
672+
prev_cycles[idx]->m_cycle_index,
673+
prev_cycles[idx]->m_snap,
674+
cycleBaseHeight);
675+
}
676+
677+
CDeterministicMNList allMns = util_params.m_dmnman.GetListForBlock(pWorkBlockIndex);
678+
const auto modifier = GetHashModifierFromWorkBlock(llmq_params, pWorkBlockIndex.get());
679+
auto newQuarterMembers = BuildNewQuorumQuarterMembers(llmq_params, util_params, allMns, previousQuarters,
680+
modifier, pWorkBlockIndex, cycleBaseHeight,
681+
/*storeSnapshot=*/false);
682+
if (newQuarterMembers.size() != nQuorums) {
683+
return std::nullopt;
684+
}
685+
686+
QuorumMembers quorumMembers;
687+
for (auto* prev_cycle : prev_cycles | std::views::reverse) {
688+
quorumMembers.insert(quorumMembers.end(),
689+
prev_cycle->m_members[quorumIndex].begin(),
690+
prev_cycle->m_members[quorumIndex].end());
691+
}
692+
quorumMembers.insert(quorumMembers.end(),
693+
newQuarterMembers[quorumIndex].begin(),
694+
newQuarterMembers[quorumIndex].end());
695+
696+
return quorumMembers;
697+
}
698+
600699
QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParameters& util_params, bool reset_cache)
601700
{
602701
static RecursiveMutex cs_members;

src/llmq/utils.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <gsl/pointers.h>
1818

19+
#include <optional>
1920
#include <unordered_set>
2021
#include <vector>
2122

@@ -75,6 +76,13 @@ std::vector<CDeterministicMNCPtr> ComputeNonRotatedQuorumMembersFromWorkBlock(
7576
Consensus::LLMQType llmqType, const CChainParams& chainparams, const CDeterministicMNList& mn_list,
7677
gsl::not_null<const CBlockIndex*> pWorkBlockIndex, int quorumHeight);
7778

79+
// Predicts the members of a future rotated v20 quorum from its already-known cycle work block,
80+
// before the next cycle base block exists on chain. Returns std::nullopt when the historical
81+
// snapshots needed for quarter rotation are not available.
82+
std::optional<std::vector<CDeterministicMNCPtr>> ComputeRotatedQuorumMembersFromWorkBlock(
83+
Consensus::LLMQType llmqType, const UtilParameters& util_params,
84+
gsl::not_null<const CBlockIndex*> pWorkBlockIndex, int quorumHeight);
85+
7886
Uint256HashSet GetQuorumConnections(const Consensus::LLMQParams& llmqParams, const CSporkManager& sporkman,
7987
const UtilParameters& util_params, const uint256& forMember, bool onlyOutbound);
8088

src/rpc/quorums.cpp

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ static RPCHelpMan quorum_dkginfo()
998998
{
999999
{RPCResult::Type::NUM, "active_dkgs", "Total number of active DKG sessions this node is participating in right now"},
10001000
{RPCResult::Type::NUM, "next_dkg", "The number of blocks until the next potential DKG session"},
1001-
{RPCResult::Type::ARR, "upcoming_dkgs", /*optional=*/true, "Upcoming DKG sessions for the given proTxHash whose work block is already mined, i.e. sessions starting within the work-diff depth",
1001+
{RPCResult::Type::ARR, "upcoming_dkgs", /*optional=*/true, "Upcoming DKG sessions for the given proTxHash whose work block is already mined. For rotated quorums all indices in a cycle share the cycle base work block",
10021002
{
10031003
{RPCResult::Type::OBJ, "", "",
10041004
{
@@ -1068,7 +1068,11 @@ static RPCHelpMan quorum_dkginfo()
10681068
if (quorumHeight <= nTipHeight) {
10691069
quorumHeight += llmq_params.dkgInterval;
10701070
}
1071-
if (quorumHeight <= nTipHeight || quorumHeight - nTipHeight > llmq::WORK_DIFF_DEPTH) {
1071+
const int cycleBaseHeight{quorumHeight - quorumIndex};
1072+
// Rotated quorums share the work block of their cycle base, so gate
1073+
// availability on the work block height rather than each index's start height
1074+
const int workHeight{(rotation_enabled ? cycleBaseHeight : quorumHeight) - llmq::WORK_DIFF_DEPTH};
1075+
if (workHeight > nTipHeight) {
10721076
continue;
10731077
}
10741078

@@ -1080,15 +1084,7 @@ static RPCHelpMan quorum_dkginfo()
10801084
obj.pushKV("blocksUntilStart", quorumHeight - nTipHeight);
10811085
obj.pushKV("proTxHash", proTxHash.ToString());
10821086

1083-
if (llmq_params.useRotation) {
1084-
obj.pushKV("known", false);
1085-
obj.pushKV("reason", "rotated quorum prediction is not exposed yet");
1086-
upcoming.push_back(obj);
1087-
continue;
1088-
}
1089-
1090-
int workHeight = quorumHeight - llmq::WORK_DIFF_DEPTH;
1091-
if (workHeight < 0 || workHeight > nTipHeight) {
1087+
if (workHeight < 0) {
10921088
obj.pushKV("known", false);
10931089
obj.pushKV("reason", "work block is not available yet");
10941090
upcoming.push_back(obj);
@@ -1103,9 +1099,25 @@ static RPCHelpMan quorum_dkginfo()
11031099
continue;
11041100
}
11051101

1106-
auto mnList = CHECK_NONFATAL(node.dmnman)->GetListForBlock(pWorkBlockIndex);
1107-
auto members = llmq::utils::ComputeNonRotatedQuorumMembersFromWorkBlock(
1108-
llmq_params.type, Params(), mnList, pWorkBlockIndex, quorumHeight);
1102+
std::vector<CDeterministicMNCPtr> members;
1103+
if (rotation_enabled) {
1104+
const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
1105+
auto predicted_members = llmq::utils::ComputeRotatedQuorumMembersFromWorkBlock(
1106+
llmq_params.type,
1107+
{*CHECK_NONFATAL(node.dmnman), *CHECK_NONFATAL(llmq_ctx.qsnapman), chainman, pindexTip},
1108+
pWorkBlockIndex, quorumHeight);
1109+
if (!predicted_members.has_value()) {
1110+
obj.pushKV("known", false);
1111+
obj.pushKV("reason", "rotated quorum snapshots are not available");
1112+
upcoming.push_back(obj);
1113+
continue;
1114+
}
1115+
members = std::move(predicted_members.value());
1116+
} else {
1117+
auto mnList = CHECK_NONFATAL(node.dmnman)->GetListForBlock(pWorkBlockIndex);
1118+
members = llmq::utils::ComputeNonRotatedQuorumMembersFromWorkBlock(
1119+
llmq_params.type, Params(), mnList, pWorkBlockIndex, quorumHeight);
1120+
}
11091121

11101122
obj.pushKV("known", true);
11111123
int memberIndex{-1};

test/functional/feature_llmq_rotation.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,8 @@ def run_test(self):
294294
assert_equal(len(default_upcoming_100), 1)
295295
assert_equal(default_upcoming_100[0]["proTxHash"], active_mn.proTxHash)
296296

297-
# Rotated quorums cannot be predicted before their snapshots exist
298-
upcoming_all = active_mn.get_node(self).quorum("dkginfo", active_mn.proTxHash)["upcoming_dkgs"]
299-
rotated_entries = [d for d in upcoming_all if d["llmqType"] == llmq_type]
300-
assert_greater_than_or_equal(len(rotated_entries), 1)
301-
for entry in rotated_entries:
302-
assert_equal(entry["known"], False)
303-
assert_equal(entry["reason"], "rotated quorum prediction is not exposed yet")
304-
305297
predicted_members = {}
298+
predicted_rotated_members = {0: {}, 1: {}}
306299
for mn in self.mninfo:
307300
dkg_info = mn.get_node(self).quorum("dkginfo", mn.proTxHash)
308301
upcoming_100 = [d for d in dkg_info["upcoming_dkgs"] if d["llmqType"] == 100]
@@ -319,15 +312,37 @@ def run_test(self):
319312
assert_equal(predicted_100["memberCount"], self.llmq_size)
320313
assert_equal(predicted_100["isMember"], predicted_100["memberIndex"] != -1)
321314
predicted_members[mn.proTxHash] = predicted_100["isMember"]
322-
assert_equal(sum(predicted_members.values()), self.llmq_size)
323315

324-
self.log.info("Mine the predicted type-100 quorum and compare selected members")
325-
quorum_hash = self.mine_quorum()
326-
quorum_info = self.nodes[0].quorum("info", 100, quorum_hash)
327-
assert_equal(quorum_info["height"], expected_quorum_height)
328-
actual_members = set(extract_quorum_members(quorum_info))
329-
predicted_member_hashes = set(proTxHash for proTxHash, is_member in predicted_members.items() if is_member)
330-
assert_equal(predicted_member_hashes, actual_members)
316+
upcoming_rotated = [d for d in dkg_info["upcoming_dkgs"] if d["llmqType"] == llmq_type]
317+
# All indices of the upcoming cycle share the cycle base work block, so
318+
# every rotated quorumIndex must be reported once that work block is mined
319+
assert_equal([d["quorumIndex"] for d in upcoming_rotated], [0, 1])
320+
for predicted_rotated in upcoming_rotated:
321+
quorum_index = predicted_rotated["quorumIndex"]
322+
assert_equal(predicted_rotated["llmqTypeName"], llmq_type_name)
323+
assert_equal(predicted_rotated["quorumHeight"], expected_quorum_height + quorum_index)
324+
assert_equal(predicted_rotated["blocksUntilStart"], expected_quorum_height + quorum_index - tip)
325+
assert_equal(predicted_rotated["proTxHash"], mn.proTxHash)
326+
assert_equal(predicted_rotated["known"], True)
327+
assert_equal(predicted_rotated["workBlockHeight"], expected_quorum_height - 8)
328+
assert_equal(predicted_rotated["workBlockHash"], self.nodes[0].getblockhash(expected_quorum_height - 8))
329+
assert_equal(predicted_rotated["memberCount"], self.llmq_size_dip0024)
330+
assert_equal(predicted_rotated["isMember"], predicted_rotated["memberIndex"] != -1)
331+
predicted_rotated_members[quorum_index][mn.proTxHash] = predicted_rotated["isMember"]
332+
assert_equal(sum(predicted_members.values()), self.llmq_size)
333+
for quorum_index in [0, 1]:
334+
assert_equal(sum(predicted_rotated_members[quorum_index].values()), self.llmq_size_dip0024)
335+
336+
self.log.info("Mine the predicted rotated quorums and compare selected members")
337+
quorum_infos_rotated = self.mine_cycle_quorum()
338+
assert_equal(quorum_infos_rotated[0]["height"], expected_quorum_height)
339+
for quorum_index, quorum_info_rotated in enumerate(quorum_infos_rotated):
340+
assert_equal(quorum_info_rotated["quorumIndex"], quorum_index)
341+
actual_rotated_members = set(extract_quorum_members(quorum_info_rotated))
342+
predicted_rotated_member_hashes = set(
343+
proTxHash for proTxHash, is_member in predicted_rotated_members[quorum_index].items() if is_member
344+
)
345+
assert_equal(predicted_rotated_member_hashes, actual_rotated_members)
331346

332347
def test_getmnlistdiff_quorums(self, baseBlockHash, blockHash, baseQuorumList, expectedDeleted, expectedNew, testQuorumsCLSigs = True):
333348
d = self.test_getmnlistdiff_base(baseBlockHash, blockHash, testQuorumsCLSigs)

0 commit comments

Comments
 (0)