Skip to content

Commit d1f5053

Browse files
authored
dash(s7): mn_state_machine leaf — providertx + mn_state_db + mn_state_machine + KAT (#352)
Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 4680887 commit d1f5053

6 files changed

Lines changed: 2085 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 test_dash_quorum test_dash_quorum_root \
75+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state \
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 test_dash_quorum test_dash_quorum_root \
208+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state \
209209
test_hash_link test_decay_pplns \
210210
test_pplns_consensus \
211211
test_v36_script_sorting test_v36_cross_impl_refhash \

src/impl/dash/coin/mn_state_db.hpp

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
#pragma once
2+
3+
/// Phase C-PAY step 2: per-MN state store.
4+
///
5+
/// Holds the EFFECTIVE state of every masternode we know about, derived
6+
/// by the per-block state machine (Phase C-PAY step 3) from observed
7+
/// ProTx records + coinbase outputs. The `MNState` struct is our pruned
8+
/// internal version of dashcore's `CDeterministicMNState`
9+
/// (evo/dmnstate.h) — same field semantics for the bits we use, but
10+
/// reduced to what GetProjectedMNPayees actually needs:
11+
///
12+
/// - nLastPaidHeight, nPoSeRevivedHeight, nRegisteredHeight
13+
/// → CompareByLastPaid_GetHeight (the sort key)
14+
/// - proRegTxHash → tiebreaker
15+
/// - nType → Regular vs Evo (Evo bonus path)
16+
/// - nConsecutivePayments → Evo "paid 4 in a row" bonus tracking
17+
/// - isValid → eligibility filter
18+
/// - scriptPayout → for "[PAY] match/mismatch" verification
19+
///
20+
/// We persist the FULL state-relevant subset (including keyIDOwner /
21+
/// pubKeyOperator / nOperatorReward / collateralOutpoint / Evo platform
22+
/// fields) so future state-machine work can consume them without a
23+
/// schema migration.
24+
///
25+
/// The store mirrors SMLDb + QuorumDb: open at
26+
/// `~/.c2pool/<coin>/mn_state_db/`, atomic batch full-rewrite per-block
27+
/// state advance, BEST sentinel for tip tracking + cross-check with
28+
/// SMLDb/QuorumDb (divergence → wipe all three).
29+
///
30+
/// Key schema:
31+
/// 'M' + proRegTxHash(32B) → pack(MNState)
32+
/// 'B' → best_hash(32B) + best_height(4B LE)
33+
///
34+
/// Persistence wire format is INTERNAL-ONLY — never shared on the
35+
/// network — so we use pack.hpp's SERIALIZE_METHODS macros directly
36+
/// without worrying about bit-exact match against dashcore's
37+
/// CDeterministicMNState wire layout.
38+
39+
#include <impl/dash/coin/vendor/providertx.hpp>
40+
41+
#include <core/leveldb_store.hpp>
42+
#include <core/uint256.hpp>
43+
#include <core/log.hpp>
44+
#include <core/opscript.hpp>
45+
#include <core/pack.hpp>
46+
#include <core/pack_types.hpp>
47+
#include <impl/bitcoin_family/coin/base_transaction.hpp>
48+
49+
#include <array>
50+
#include <cstdint>
51+
#include <cstring>
52+
#include <span>
53+
#include <string>
54+
#include <vector>
55+
56+
namespace dash {
57+
namespace coin {
58+
59+
struct MNState
60+
{
61+
// From CProRegTx (registration — immutable per MN unless a later
62+
// ProUpRegTx rewrites them)
63+
uint16_t nVersion{vendor::ProTxVersion::BASIC_BLS};
64+
uint8_t nType{vendor::MnType::REGULAR};
65+
bitcoin_family::coin::TxPrevOut collateralOutpoint;
66+
uint160 keyIDOwner;
67+
std::array<uint8_t, vendor::BLS_PUBKEY_SIZE> pubKeyOperator{};
68+
uint160 keyIDVoting;
69+
uint16_t nOperatorReward{0};
70+
OPScript scriptPayout;
71+
72+
// From CProUpServTx
73+
vendor::LegacyNetService netInfo;
74+
OPScript scriptOperatorPayout;
75+
76+
// Per-block state-machine outputs
77+
uint32_t nRegisteredHeight{0};
78+
uint32_t nLastPaidHeight{0};
79+
uint32_t nPoSeRevivedHeight{0};
80+
uint32_t nPoSeBanHeight{0};
81+
uint32_t nConsecutivePayments{0};
82+
uint16_t nRevocationReason{vendor::CProUpRevTx::REASON_NOT_SPECIFIED};
83+
bool isValid{true};
84+
85+
// Evo-only platform fields (gated on nType == EVO)
86+
uint160 platformNodeID;
87+
uint16_t platformP2PPort{0};
88+
uint16_t platformHTTPPort{0};
89+
90+
C2POOL_SERIALIZE_METHODS(MNState)
91+
{
92+
READWRITE(obj.nVersion,
93+
obj.nType,
94+
obj.collateralOutpoint,
95+
obj.keyIDOwner,
96+
Using<vendor::RawBytesFormat<vendor::BLS_PUBKEY_SIZE>>(obj.pubKeyOperator),
97+
obj.keyIDVoting,
98+
obj.nOperatorReward,
99+
obj.scriptPayout,
100+
obj.netInfo,
101+
obj.scriptOperatorPayout,
102+
obj.nRegisteredHeight,
103+
obj.nLastPaidHeight,
104+
obj.nPoSeRevivedHeight,
105+
obj.nPoSeBanHeight,
106+
obj.nConsecutivePayments,
107+
obj.nRevocationReason,
108+
obj.isValid,
109+
obj.platformNodeID,
110+
obj.platformP2PPort,
111+
obj.platformHTTPPort);
112+
}
113+
114+
bool operator==(const MNState& r) const
115+
{
116+
return nVersion == r.nVersion
117+
&& nType == r.nType
118+
&& nRegisteredHeight == r.nRegisteredHeight
119+
&& nLastPaidHeight == r.nLastPaidHeight
120+
&& nPoSeRevivedHeight == r.nPoSeRevivedHeight
121+
&& nPoSeBanHeight == r.nPoSeBanHeight
122+
&& nConsecutivePayments == r.nConsecutivePayments
123+
&& nRevocationReason == r.nRevocationReason
124+
&& isValid == r.isValid
125+
&& nOperatorReward == r.nOperatorReward
126+
&& keyIDOwner == r.keyIDOwner
127+
&& pubKeyOperator == r.pubKeyOperator
128+
&& keyIDVoting == r.keyIDVoting
129+
&& scriptPayout.m_data == r.scriptPayout.m_data
130+
&& scriptOperatorPayout.m_data == r.scriptOperatorPayout.m_data
131+
&& platformNodeID == r.platformNodeID
132+
&& platformP2PPort == r.platformP2PPort
133+
&& platformHTTPPort == r.platformHTTPPort;
134+
}
135+
};
136+
137+
class MnStateDb
138+
{
139+
public:
140+
explicit MnStateDb(const std::string& db_path,
141+
const ::core::LevelDBOptions& opts = {})
142+
: m_store(db_path, opts) {}
143+
144+
bool open()
145+
{
146+
if (!m_store.open()) return false;
147+
load_best_state();
148+
LOG_INFO << "[MNS-DB] opened best_height=" << m_best_height
149+
<< " best_hash=" << m_best_hash.GetHex().substr(0, 16);
150+
return true;
151+
}
152+
153+
void close() { m_store.close(); }
154+
bool is_open() const { return m_store.is_open(); }
155+
156+
uint256 get_best_hash() const { return m_best_hash; }
157+
uint32_t get_best_height() const { return m_best_height; }
158+
159+
// Load every persisted MNState into `out` keyed by proRegTxHash.
160+
// Returns true if at least one entry loaded OR the BEST sentinel
161+
// exists (warm restart with empty set is technically valid).
162+
bool load_all(std::vector<std::pair<uint256, MNState>>& out)
163+
{
164+
out.clear();
165+
auto keys = m_store.list_keys(std::string(1, 'M'),
166+
/*limit=*/100000);
167+
out.reserve(keys.size());
168+
size_t bad = 0;
169+
for (const auto& key : keys) {
170+
if (key.size() != 33 || key[0] != 'M') continue;
171+
std::vector<uint8_t> data;
172+
if (!m_store.get(key, data)) { ++bad; continue; }
173+
try {
174+
MNState s;
175+
::PackStream ps(data);
176+
ps >> s;
177+
uint256 h;
178+
std::memcpy(h.data(), key.data() + 1, 32);
179+
out.emplace_back(h, std::move(s));
180+
} catch (const std::exception& ex) {
181+
++bad;
182+
LOG_WARNING << "[MNS-DB] entry deserialize failed: "
183+
<< ex.what();
184+
}
185+
}
186+
if (bad > 0) {
187+
LOG_WARNING << "[MNS-DB] " << bad
188+
<< " entries failed to load (skipped)";
189+
}
190+
bool warm = !out.empty() || !m_best_hash.IsNull();
191+
if (warm) {
192+
LOG_INFO << "[MNS-DB] loaded " << out.size()
193+
<< " entries best_height=" << m_best_height;
194+
}
195+
return warm;
196+
}
197+
198+
// Atomic full-rewrite of the entire MN state set + BEST sentinel.
199+
// Same pattern as SMLDb::write_sml. ~3000 MNs × ~220 B avg ≈
200+
// 660 KB per-block rewrite. Fine — LevelDB compaction handles
201+
// the churn on the same order as SML rewrites.
202+
bool write_all(const std::vector<std::pair<uint256, MNState>>& entries,
203+
const uint256& best_hash,
204+
uint32_t best_height)
205+
{
206+
// Monotonic-advance: best_height only ever increases. Required so
207+
// that bootstrap drain (replaying h=snapshot+1 .. tip in chain
208+
// order, AFTER a tip block at top-of-handler may have already
209+
// bumped best_height to tip-height) does not roll back the
210+
// persisted high-watermark. Snapshot ENTRIES still need to be
211+
// written, so we always persist `entries`; only the best_hash /
212+
// best_height update is gated.
213+
const bool advance = (best_height >= m_best_height);
214+
215+
auto batch = m_store.create_batch();
216+
217+
auto existing = m_store.list_keys(std::string(1, 'M'),
218+
/*limit=*/100000);
219+
for (const auto& k : existing) batch.remove(k);
220+
221+
for (const auto& [hash, state] : entries) {
222+
auto key = make_entry_key(hash);
223+
auto stream = ::pack(state);
224+
auto sp = stream.get_span();
225+
std::vector<uint8_t> data(
226+
reinterpret_cast<const uint8_t*>(sp.data()),
227+
reinterpret_cast<const uint8_t*>(sp.data()) + sp.size());
228+
batch.put(key, data);
229+
}
230+
231+
if (advance) {
232+
batch.put(make_state_key(),
233+
encode_best_state(best_hash, best_height));
234+
}
235+
236+
if (!batch.commit()) {
237+
LOG_WARNING << "[MNS-DB] write_all batch commit failed";
238+
return false;
239+
}
240+
if (advance) {
241+
m_best_hash = best_hash;
242+
m_best_height = best_height;
243+
}
244+
return true;
245+
}
246+
247+
bool clear()
248+
{
249+
auto batch = m_store.create_batch();
250+
for (auto& k : m_store.list_keys(std::string(1, 'M'), 100000))
251+
batch.remove(k);
252+
batch.remove(make_state_key());
253+
if (!batch.commit()) return false;
254+
m_best_hash = uint256{};
255+
m_best_height = 0;
256+
LOG_INFO << "[MNS-DB] cleared";
257+
return true;
258+
}
259+
260+
private:
261+
::core::LevelDBStore m_store;
262+
uint256 m_best_hash;
263+
uint32_t m_best_height{0};
264+
265+
static std::string make_entry_key(const uint256& proRegTxHash)
266+
{
267+
std::string k;
268+
k.reserve(33);
269+
k.push_back('M');
270+
k.append(reinterpret_cast<const char*>(proRegTxHash.data()), 32);
271+
return k;
272+
}
273+
274+
static std::string make_state_key() { return std::string(1, 'B'); }
275+
276+
static std::vector<uint8_t> encode_best_state(const uint256& hash,
277+
uint32_t height)
278+
{
279+
std::vector<uint8_t> out(36);
280+
std::memcpy(out.data(), hash.data(), 32);
281+
out[32] = static_cast<uint8_t>( height & 0xFF);
282+
out[33] = static_cast<uint8_t>((height >> 8) & 0xFF);
283+
out[34] = static_cast<uint8_t>((height >> 16) & 0xFF);
284+
out[35] = static_cast<uint8_t>((height >> 24) & 0xFF);
285+
return out;
286+
}
287+
288+
void load_best_state()
289+
{
290+
std::vector<uint8_t> data;
291+
if (!m_store.get(make_state_key(), data) || data.size() < 36) {
292+
return;
293+
}
294+
std::memcpy(m_best_hash.data(), data.data(), 32);
295+
m_best_height = uint32_t(data[32])
296+
| (uint32_t(data[33]) << 8)
297+
| (uint32_t(data[34]) << 16)
298+
| (uint32_t(data[35]) << 24);
299+
}
300+
};
301+
302+
} // namespace coin
303+
} // namespace dash

0 commit comments

Comments
 (0)