|
| 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