Skip to content

Commit 67b309f

Browse files
authored
Merge pull request #201 from frstrtr/nmc/p1f-leveldb-persist
nmc(P1f): HeaderChain LevelDB persistence (h/tip/height) + 6 KATs (52->58)
2 parents 771f586 + 19c00f8 commit 67b309f

2 files changed

Lines changed: 363 additions & 2 deletions

File tree

src/impl/nmc/coin/header_chain.hpp

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,15 @@
4141
#include <core/hash.hpp>
4242
#include <core/log.hpp>
4343
#include <core/target_utils.hpp> // chain::bits_to_target (parent-PoW, step 4)
44+
#include <core/leveldb_store.hpp> // P1f: core::LevelDBStore persistence
4445

4546
#include <algorithm>
4647
#include <atomic>
4748
#include <cstdint>
49+
#include <cstring>
4850
#include <functional>
4951
#include <mutex>
52+
#include <memory>
5053
#include <optional>
5154
#include <string>
5255
#include <unordered_map>
@@ -553,6 +556,78 @@ struct IndexEntry {
553556
std::optional<AuxPow> auxpow; // merge-mining proof (P0: stored, unverified)
554557
};
555558

559+
/// P1f on-disk layout for an IndexEntry (mirror of btc::coin::IndexEntryDiskV1,
560+
/// adapted to NMC). Serialize order: header, block_hash, height, chain_work,
561+
/// status (as uint32_t), then the AuxPow presence flag (uint8_t: 1 if the entry
562+
/// carried a merge-mining proof, else 0).
563+
///
564+
/// P1f-DEFER: auxpow blob persistence -- next sub-leg. nmc::coin::AuxPow itself
565+
/// has NO ::Serialize/::Unserialize / C2POOL_SERIALIZE_METHODS, so per the
566+
/// fence we do NOT hand-roll its wire layout here. This leg persists ONLY the
567+
/// presence flag; on load the AuxPow is reconstructed as std::nullopt, but the
568+
/// entry's status (HEADER_VALID_CHAIN for a merge-mined header) is preserved
569+
/// faithfully so the restored chain's validation state is not lost. The actual
570+
/// proof blob lands in the next sub-leg behind this same flag.
571+
struct IndexEntryDiskV1 {
572+
BlockHeaderType header;
573+
uint256 block_hash; // SHA256d(header)
574+
uint32_t height{0};
575+
uint256 chain_work; // cumulative work up to this header
576+
HeaderStatus status{HEADER_VALID_UNKNOWN};
577+
uint8_t has_auxpow{0}; // presence flag (blob deferred -- see above)
578+
579+
template<typename Stream>
580+
void Serialize(Stream& s) const {
581+
::Serialize(s, header);
582+
::Serialize(s, block_hash);
583+
::Serialize(s, height);
584+
::Serialize(s, chain_work);
585+
::Serialize(s, static_cast<uint32_t>(status));
586+
::Serialize(s, has_auxpow);
587+
// P1f-DEFER: auxpow blob persistence -- next sub-leg. Nothing follows the
588+
// flag yet; when AuxPow gains serialization the blob is written here iff
589+
// has_auxpow == 1.
590+
}
591+
template<typename Stream>
592+
void Unserialize(Stream& s) {
593+
::Unserialize(s, header);
594+
::Unserialize(s, block_hash);
595+
::Unserialize(s, height);
596+
::Unserialize(s, chain_work);
597+
uint32_t st;
598+
::Unserialize(s, st);
599+
status = static_cast<HeaderStatus>(st);
600+
::Unserialize(s, has_auxpow);
601+
// P1f-DEFER: auxpow blob persistence -- next sub-leg. The blob is not on
602+
// disk yet, so there is nothing further to read behind the flag.
603+
}
604+
605+
/// Materialize the slim in-memory form. auxpow is reconstructed as nullopt
606+
/// in this leg (blob deferred); status carries the merge-mined verdict.
607+
IndexEntry to_entry() const {
608+
IndexEntry e;
609+
e.header = header;
610+
e.block_hash = block_hash;
611+
e.height = height;
612+
e.chain_work = chain_work;
613+
e.status = status;
614+
e.auxpow = std::nullopt; // P1f-DEFER: blob not persisted yet
615+
return e;
616+
}
617+
618+
/// Build the on-disk form from a slim entry.
619+
static IndexEntryDiskV1 from_entry(const IndexEntry& e) {
620+
IndexEntryDiskV1 d;
621+
d.header = e.header;
622+
d.block_hash = e.block_hash;
623+
d.height = e.height;
624+
d.chain_work = e.chain_work;
625+
d.status = e.status;
626+
d.has_auxpow = e.auxpow.has_value() ? 1 : 0;
627+
return d;
628+
}
629+
};
630+
556631
/// Work represented by a compact target. Mirror of btc::coin::get_block_proof
557632
/// (src/impl/btc/coin/header_chain.hpp) — NMC is a SHA256d Bitcoin fork, so the
558633
/// work metric is identical. Kept local to the NMC lane (btc tree is read-only;
@@ -591,9 +666,24 @@ class HeaderChain {
591666
/// P0-DEFER: no LevelDB open, no persisted-state load, no checkpoint seed.
592667
/// Returns true (structural success) so wiring smoke-tests pass.
593668
bool init() {
594-
LOG_INFO << "[EMB-NMC] HeaderChain::init() (P0 structural stub) db_path="
669+
LOG_INFO << "[EMB-NMC] HeaderChain::init() db_path="
595670
<< (m_db_path.empty() ? "(in-memory)" : m_db_path);
596-
// P0-DEFER: persistence + checkpoint-seed not implemented.
671+
if (m_db_path.empty())
672+
return true; // pure in-memory mode
673+
674+
core::LevelDBOptions opts;
675+
opts.write_buffer_size = 2 * 1024 * 1024; // 2MB
676+
opts.block_cache_size = 4 * 1024 * 1024; // 4MB
677+
m_db = std::make_unique<core::LevelDBStore>(m_db_path, opts);
678+
if (!m_db->open()) {
679+
// Non-fatal (mirror of btc): degrade to in-memory and keep running.
680+
LOG_WARNING << "[EMB-NMC] HeaderChain LevelDB open FAILED at "
681+
<< m_db_path << " -- continuing in-memory";
682+
m_db.reset();
683+
return true;
684+
}
685+
LOG_INFO << "[EMB-NMC] HeaderChain LevelDB opened at " << m_db_path;
686+
load_from_db();
597687
return true;
598688
}
599689

@@ -829,6 +919,7 @@ class HeaderChain {
829919
// min-difficulty testnet where every block carries identical work.
830920
// Re-receiving a stored header is the idempotent reject above, so the
831921
// switch cannot flip-flop.
922+
bool tip_changed = false;
832923
const bool first = m_tip.IsNull();
833924
const bool dominated = entry_work > m_best_work;
834925
const bool equal_at_tip = entry_work == m_best_work
@@ -840,6 +931,7 @@ class HeaderChain {
840931
m_tip = bh;
841932
m_tip_height = static_cast<uint32_t>(height);
842933
m_best_work = entry_work; // cumulative work at the new tip
934+
tip_changed = true;
843935
if (!first && (equal_at_tip
844936
|| height <= static_cast<int32_t>(old_height))) {
845937
LOG_WARNING << "[EMB-NMC] "
@@ -849,11 +941,111 @@ class HeaderChain {
849941
<< " new_tip=" << bh.ToString().substr(0, 16);
850942
}
851943
}
944+
945+
// P1f: persist the newly-accepted header (and, if the tip moved, the
946+
// tip/height pointers) atomically. Only on this success path -- the
947+
// idempotent / orphan / activation-gate early returns above never reach
948+
// here, so nothing is written for a rejected header.
949+
persist_entry_locked(bh, tip_changed);
852950
return true;
853951
}
854952

953+
// P1f: write a single accepted entry to LevelDB in one synced batch. Caller
954+
// holds m_mutex and has already inserted the entry into m_index. No-op when
955+
// running in pure in-memory mode (m_db null / not open).
956+
void persist_entry_locked(const uint256& bh, bool tip_changed) {
957+
if (!m_db || !m_db->is_open()) return;
958+
auto eit = m_index.find(bh);
959+
if (eit == m_index.end()) return; // defensive -- should never miss
960+
961+
auto batch = m_db->create_batch();
962+
963+
// "h" + block_hash(32 raw bytes) -> serialized IndexEntryDiskV1.
964+
auto disk = IndexEntryDiskV1::from_entry(eit->second);
965+
auto packed = pack(disk);
966+
std::vector<uint8_t> data(
967+
reinterpret_cast<const uint8_t*>(packed.data()),
968+
reinterpret_cast<const uint8_t*>(packed.data()) + packed.size());
969+
std::string hkey = "h";
970+
hkey.append(reinterpret_cast<const char*>(bh.data()), 32);
971+
batch.put(hkey, data);
972+
973+
if (tip_changed) {
974+
std::vector<uint8_t> tip_data(m_tip.data(), m_tip.data() + 32);
975+
batch.put("tip", tip_data);
976+
977+
const uint32_t h = m_tip_height;
978+
std::vector<uint8_t> height_data(4);
979+
height_data[0] = static_cast<uint8_t>((h >> 24) & 0xFF);
980+
height_data[1] = static_cast<uint8_t>((h >> 16) & 0xFF);
981+
height_data[2] = static_cast<uint8_t>((h >> 8) & 0xFF);
982+
height_data[3] = static_cast<uint8_t>( h & 0xFF);
983+
batch.put("height", height_data);
984+
}
985+
986+
if (!batch.commit_sync())
987+
LOG_ERROR << "[EMB-NMC] persist_entry_locked: WriteBatch FAILED for "
988+
<< bh.ToString().substr(0, 16);
989+
}
990+
991+
// P1f: restore m_index / m_tip / m_tip_height / m_best_work from LevelDB.
992+
// Schema (NMC has no height-index, so NO "i" key):
993+
// "h" + block_hash(32 raw bytes) -> serialized IndexEntryDiskV1
994+
// "tip" -> block_hash(32 raw bytes)
995+
// "height" -> uint32_t, 4 bytes big-endian
996+
// Caller is init() before any concurrent access; no lock held / needed.
997+
void load_from_db() {
998+
if (!m_db || !m_db->is_open()) return;
999+
1000+
// Enumerate every stored header under the "h" prefix and rebuild m_index.
1001+
auto h_keys = m_db->list_keys("h", 10000000);
1002+
size_t loaded = 0;
1003+
for (auto& k : h_keys) {
1004+
if (k.size() != 33 || k[0] != 'h') continue; // "h" + 32 raw bytes
1005+
std::vector<uint8_t> data;
1006+
if (!m_db->get(k, data)) continue;
1007+
try {
1008+
PackStream ps(data);
1009+
IndexEntryDiskV1 disk;
1010+
ps >> disk;
1011+
IndexEntry e = disk.to_entry();
1012+
const uint256 bh = e.block_hash;
1013+
m_index.emplace(bh, std::move(e));
1014+
++loaded;
1015+
} catch (const std::exception& ex) {
1016+
LOG_WARNING << "[EMB-NMC] load_from_db: corrupt header entry, skipped: "
1017+
<< ex.what();
1018+
}
1019+
}
1020+
1021+
// Restore the tip pointer (32 raw bytes) and height (4 BE bytes).
1022+
std::vector<uint8_t> tip_data;
1023+
if (m_db->get("tip", tip_data) && tip_data.size() == 32) {
1024+
std::memcpy(m_tip.data(), tip_data.data(), 32);
1025+
auto tit = m_index.find(m_tip);
1026+
if (tit == m_index.end()) {
1027+
LOG_WARNING << "[EMB-NMC] load_from_db: tip hash not in index -- resetting";
1028+
m_tip.SetNull();
1029+
} else {
1030+
m_best_work = tit->second.chain_work; // recompute best-work from tip
1031+
}
1032+
}
1033+
std::vector<uint8_t> height_data;
1034+
if (!m_tip.IsNull() && m_db->get("height", height_data) && height_data.size() == 4) {
1035+
m_tip_height = (static_cast<uint32_t>(height_data[0]) << 24)
1036+
| (static_cast<uint32_t>(height_data[1]) << 16)
1037+
| (static_cast<uint32_t>(height_data[2]) << 8)
1038+
| static_cast<uint32_t>(height_data[3]);
1039+
}
1040+
1041+
LOG_INFO << "[EMB-NMC] load_from_db: loaded " << loaded << " headers"
1042+
<< " tip=" << (m_tip.IsNull() ? "(null)" : m_tip.ToString().substr(0, 16))
1043+
<< " height=" << m_tip_height;
1044+
}
1045+
8551046
NMCChainParams m_params;
8561047
std::string m_db_path;
1048+
std::unique_ptr<core::LevelDBStore> m_db; // P1f: null in pure in-memory mode
8571049

8581050
mutable std::mutex m_mutex;
8591051
std::unordered_map<uint256, IndexEntry, Uint256Hasher> m_index;

0 commit comments

Comments
 (0)