Skip to content

Commit f2cd439

Browse files
authored
Merge pull request #321 from frstrtr/dash/s7-quorum-llmq-leaf
test(dash): LLMQ quorum-tail verification leaf (S7)
2 parents 5717adf + d87a1eb commit f2cd439

6 files changed

Lines changed: 1008 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
test_threading test_weights \
7373
test_header_chain test_mempool test_template_builder \
7474
test_doge_chain test_compact_blocks test_dash_x11_kat \
75-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns \
75+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum \
7676
test_multiaddress_pplns test_pplns_stress \
7777
test_hash_link test_decay_pplns \
7878
test_pplns_consensus \
@@ -205,7 +205,7 @@ jobs:
205205
test_threading test_weights \
206206
test_header_chain test_mempool test_template_builder \
207207
test_doge_chain test_compact_blocks test_dash_x11_kat \
208-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns \
208+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum \
209209
test_hash_link test_decay_pplns \
210210
test_pplns_consensus \
211211
test_v36_script_sorting test_v36_cross_impl_refhash \
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#pragma once
2+
3+
// QuorumManager: in-memory tracker for the active LLMQ quorum set.
4+
// Phase C-QUO step 3.
5+
//
6+
// Consumes the structured QuorumTail from each accepted mnlistdiff:
7+
// - Erase entries whose (llmqType, quorumHash) appears in deletedQuorums
8+
// - For each entry in newQuorums: insert/replace by (llmqType, quorumHash)
9+
// - Cache the latest quorumsCLSigs for Phase L's ChainLock-cycle
10+
// verifier to reach (it indexes into newQuorums by uint16 position
11+
// within the most-recent diff).
12+
//
13+
// Persistence is via QuorumDb (sister of SMLDb): on every accepted
14+
// mnlistdiff we apply() in-memory, then flush state to LevelDB. On
15+
// startup main_dash loads from QuorumDb via replace_state() before any
16+
// diff arrives. Without persistence, ChainLock verify returns NO_POOL
17+
// after every restart until a cold-start mnlistdiff(zero, tip) refills
18+
// the active set — which never happens in steady state because the
19+
// next diff is incremental.
20+
//
21+
// Thread-safety: all mutation goes through the on_mnlistdiff handler
22+
// which posts to ioc; reads from other threads (Phase L's clsig
23+
// verifier) need a shared_mutex once that lands. For now we assume
24+
// single-threaded ioc access.
25+
26+
#include <impl/dash/coin/vendor/llmq_commitment.hpp>
27+
#include <impl/dash/coin/vendor/quorum_tail.hpp>
28+
29+
#include <core/uint256.hpp>
30+
#include <core/log.hpp>
31+
32+
#include <algorithm>
33+
#include <cstdint>
34+
#include <optional>
35+
#include <unordered_map>
36+
#include <utility>
37+
#include <vector>
38+
39+
namespace dash {
40+
namespace coin {
41+
42+
class QuorumManager
43+
{
44+
public:
45+
// Composite key for active-set lookup. We use a flat vector instead
46+
// of an unordered_map because we expect ~100-200 active quorums on
47+
// mainnet at any time and lookups are infrequent (Phase L's
48+
// ChainLock verifier will dominate read traffic).
49+
struct ActiveKey {
50+
uint8_t llmqType;
51+
uint256 quorumHash;
52+
53+
bool operator==(const ActiveKey& r) const
54+
{
55+
return llmqType == r.llmqType && quorumHash == r.quorumHash;
56+
}
57+
};
58+
59+
struct Entry {
60+
ActiveKey key;
61+
vendor::CFinalCommitment commitment;
62+
// Phase C-TEMPLATE step 4 prep: the block height where this
63+
// quorum's qfcommit tx was mined. Populated by the
64+
// [QC-MINED] scanner in main_dash on_full_block when the tx
65+
// is observed. 0 = unknown (mnlistdiff added the quorum but
66+
// we haven't yet seen the corresponding qfcommit tx — happens
67+
// for quorums added before our header checkpoint, or while
68+
// bootstrap is still draining).
69+
//
70+
// dashcore's GetMinedCommitmentsUntilBlock orders quorums by
71+
// mining height (newest first per llmqType), which feeds
72+
// CalcCbTxMerkleRootQuorums. Without this field we can't
73+
// mirror that ordering in step 4c's compute_merkle_root_quorums.
74+
//
75+
// NOT YET PERSISTED. QuorumDb still serializes only the
76+
// commitment; on restart, mining_height resets to 0 and
77+
// repopulates as we observe blocks. Persistence lands in the
78+
// same commit as compute_merkle_root_quorums (when it
79+
// actually starts to matter).
80+
uint32_t mining_height{0};
81+
};
82+
83+
// Look up an entry by (llmqType, quorumHash) and return a mutable
84+
// pointer for in-place state updates (used by the qfcommit
85+
// scanner to set mining_height when a type-6 tx is observed).
86+
// Returns nullptr if not in active set.
87+
Entry* find_mutable(uint8_t llmqType, const uint256& quorumHash)
88+
{
89+
for (auto& e : m_active) {
90+
if (e.key.llmqType == llmqType
91+
&& e.key.quorumHash == quorumHash) {
92+
return &e;
93+
}
94+
}
95+
return nullptr;
96+
}
97+
98+
// Apply a parsed quorum tail. Mutates the active set in place.
99+
// Returns the count of inserts/replaces and deletes for logging.
100+
struct ApplyResult {
101+
size_t added_or_updated{0};
102+
size_t deleted{0};
103+
size_t cl_sigs_cached{0};
104+
size_t active_after{0};
105+
};
106+
107+
ApplyResult apply(const vendor::QuorumTail& tail)
108+
{
109+
ApplyResult r;
110+
111+
if (!tail.deletedQuorums.empty()) {
112+
auto before = m_active.size();
113+
m_active.erase(
114+
std::remove_if(m_active.begin(), m_active.end(),
115+
[&](const Entry& e) {
116+
for (const auto& [t, h] : tail.deletedQuorums) {
117+
if (e.key.llmqType == t
118+
&& e.key.quorumHash == h) {
119+
return true;
120+
}
121+
}
122+
return false;
123+
}),
124+
m_active.end());
125+
r.deleted = before - m_active.size();
126+
}
127+
128+
for (const auto& nq : tail.newQuorums) {
129+
ActiveKey k{nq.llmqType, nq.quorumHash};
130+
auto it = std::find_if(m_active.begin(), m_active.end(),
131+
[&](const Entry& e) { return e.key == k; });
132+
if (it != m_active.end()) {
133+
it->commitment = nq;
134+
} else {
135+
m_active.push_back(Entry{k, nq});
136+
}
137+
++r.added_or_updated;
138+
}
139+
140+
// Replace cached CL sigs each diff (the previous diff's sigs
141+
// are now stale — they refer to that diff's newQuorums-order
142+
// indices, not ours).
143+
m_latest_cl_sigs = tail.quorumsCLSigs;
144+
r.cl_sigs_cached = m_latest_cl_sigs.size();
145+
r.active_after = m_active.size();
146+
return r;
147+
}
148+
149+
// Look up a commitment by (llmqType, quorumHash). Used by Phase L's
150+
// ChainLock verifier: given a clsig blob and the quorum that should
151+
// have signed it, fetch the quorum public key for verification.
152+
std::optional<vendor::CFinalCommitment>
153+
find(uint8_t llmqType, const uint256& quorumHash) const
154+
{
155+
for (const auto& e : m_active) {
156+
if (e.key.llmqType == llmqType
157+
&& e.key.quorumHash == quorumHash) {
158+
return e.commitment;
159+
}
160+
}
161+
return std::nullopt;
162+
}
163+
164+
size_t active_count() const { return m_active.size(); }
165+
166+
// Expose active entries by const reference for iteration. Used by
167+
// chainlock_verify.hpp to walk the LLMQ_400_60 candidates when
168+
// selecting the signing quorum.
169+
const std::vector<Entry>& active_entries() const { return m_active; }
170+
171+
// Per-type counts for logging / dashboard.
172+
std::unordered_map<uint8_t, size_t> active_by_type() const
173+
{
174+
std::unordered_map<uint8_t, size_t> out;
175+
for (const auto& e : m_active) ++out[e.key.llmqType];
176+
return out;
177+
}
178+
179+
void clear()
180+
{
181+
m_active.clear();
182+
m_latest_cl_sigs.clear();
183+
}
184+
185+
// Latest cached quorumsCLSigs from the most recent diff. Each entry
186+
// is (96-byte recovered BLS sig, vector<uint16_t> indices into the
187+
// most-recent diff's newQuorums). Persistence flushes this so a
188+
// restart between two mnlistdiffs doesn't lose the cycle map Phase L
189+
// reaches into.
190+
using CLSig = std::pair<
191+
std::array<uint8_t, vendor::CFinalCommitment::BLS_SIG_SIZE>,
192+
std::vector<uint16_t>>;
193+
194+
const std::vector<CLSig>& latest_cl_sigs() const
195+
{
196+
return m_latest_cl_sigs;
197+
}
198+
199+
// Bulk-replace state — used by QuorumDb::load_into() to warm the
200+
// active set + latest CL sigs from disk before the first incremental
201+
// diff arrives. On post-restart steady state this avoids needing a
202+
// full cold-start mnlistdiff(zero, tip).
203+
void replace_state(std::vector<Entry>&& active,
204+
std::vector<CLSig>&& cl_sigs)
205+
{
206+
m_active = std::move(active);
207+
m_latest_cl_sigs = std::move(cl_sigs);
208+
}
209+
210+
private:
211+
std::vector<Entry> m_active;
212+
213+
// Most-recent diff's quorumsCLSigs — kept verbatim for Phase L's
214+
// cycle-shuffle verifier. Indices reference m_active positions of
215+
// the entries inserted from THIS diff; on the next diff they go
216+
// stale and we replace them.
217+
std::vector<CLSig> m_latest_cl_sigs;
218+
};
219+
220+
} // namespace coin
221+
} // namespace dash

0 commit comments

Comments
 (0)