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