@@ -131,6 +131,40 @@ inline std::string bits_to_hex(uint32_t bits) {
131131 return std::string (buf);
132132}
133133
134+ // Underfill guard (v36 cutover deploy path)
135+ //
136+ // Port of the LTC/DOGE template-builder guard (src/impl/ltc/coin/
137+ // template_builder.hpp, src/impl/doge/coin/template_builder.hpp) to the NMC
138+ // embedded template path, in the DASH free-predicate form
139+ // (src/impl/dash/coin/embedded_gbt.hpp) — same shape as the BTC port
140+ // (src/impl/btc/coin/template_builder.hpp). Detects the "near-empty template
141+ // on a non-empty mempool" regression: the tx selector returns almost no
142+ // transactions even though the local mempool holds a substantial fee-paying
143+ // backlog. c2pool-side template-fill safety net — NOT the byte-parity KAT
144+ // axis; thresholds are the v36-native shared structure (standardize
145+ // cross-coin) pinned to the legacy p2pool near-empty floor (~50 kB),
146+ // identical to LTC/DOGE/DASH/BTC. NMC is BTC's merge-mined aux child, so a
147+ // near-empty aux block does not waste parent PoW (severity LOW) — this port
148+ // completes the all-coin underfill matrix.
149+ inline constexpr uint64_t UNDERFILL_MIN_FILL_BYTES = 50'000ull ; // < this = near-empty block
150+ inline constexpr uint64_t UNDERFILL_BACKLOG_SLACK = 50'000ull ; // unselected fee-paying material that should have filled it
151+
152+ // / Pure trip predicate — the exact boolean the LTC/DOGE guards evaluate.
153+ // / Factored out so the KAT can pin it without a log scraper:
154+ // / near_empty : template packed fewer bytes than the near-empty floor
155+ // / has_backlog : the mempool holds fee-paying material (known fees > 0)
156+ // / well beyond what was selected (> selected + slack)
157+ // / Genuinely empty (or fee-unknown-only) mempools never trip.
158+ inline bool underfill_guard_trips (uint64_t selected_bytes,
159+ uint64_t mempool_bytes,
160+ uint64_t mempool_known_fees)
161+ {
162+ const bool near_empty = selected_bytes < UNDERFILL_MIN_FILL_BYTES ;
163+ const bool has_backlog = mempool_known_fees > 0
164+ && mempool_bytes > selected_bytes + UNDERFILL_BACKLOG_SLACK ;
165+ return near_empty && has_backlog;
166+ }
167+
134168// TemplateBuilder
135169
136170// / Builds an NMC block template from a validated HeaderChain and Mempool.
@@ -165,10 +199,16 @@ class TemplateBuilder {
165199
166200 // / Build a WorkData template from the current chain tip + mempool.
167201 // / Returns std::nullopt if the chain has no tip yet (not synced to genesis).
202+ // / underfill_tripped: optional underfill-guard observation seam. Defaults
203+ // / to nullptr so every existing caller is byte-for-byte unchanged
204+ // / (SAFE-ADDITIVE); the guard KAT passes a bool to pin the wiring without
205+ // / a log scraper. The guard itself is log-only (WARNING), exactly like
206+ // / LTC/DOGE/BTC — it never alters the template.
168207 static std::optional<rpc::WorkData> build_template (
169208 const HeaderChain& chain,
170209 const Mempool& pool,
171- bool is_testnet = false )
210+ bool is_testnet = false ,
211+ bool * underfill_tripped = nullptr )
172212 {
173213 (void )is_testnet; // reserved for future per-network rules
174214 auto t0 = std::chrono::steady_clock::now ();
@@ -224,9 +264,11 @@ class TemplateBuilder {
224264 std::vector<Transaction> tx_objects;
225265 std::vector<uint256> tx_hashes;
226266
267+ uint64_t selected_bytes = 0 ; // wire bytes packed into this template (underfill guard)
227268 for (const auto & stx : selected_txs) {
228269 uint256 txid = compute_txid (stx.tx );
229270 auto packed = pack (TX_WITH_WITNESS (stx.tx ));
271+ selected_bytes += packed.get_span ().size ();
230272 std::string hex_data = HexStr (packed.get_span ());
231273 // wtxid = SHA256d of witness serialization (for witness merkle tree)
232274 uint256 wtxid = Hash (packed.get_span ());
@@ -245,6 +287,32 @@ class TemplateBuilder {
245287 tx_hashes.push_back (txid);
246288 }
247289
290+ // Underfill guard
291+ //
292+ // Do not silently treat a near-empty template as healthy when the
293+ // mempool held fee-paying backlog that should have filled it. We cannot
294+ // fabricate transactions, so we surface loudly (WARNING) for
295+ // contabo-prod-watch / the operator rather than shipping a false-empty
296+ // block as normal. Genuinely empty mempools never trip this. Mirrors
297+ // the LTC/DOGE/BTC TemplateBuilder guard; additive only — the GBT JSON
298+ // below is untouched either way.
299+ {
300+ const uint64_t mempool_bytes = static_cast <uint64_t >(pool.byte_size ());
301+ const uint64_t mempool_fees = pool.total_fees ();
302+ const bool tripped = underfill_guard_trips (selected_bytes,
303+ mempool_bytes,
304+ mempool_fees);
305+ if (underfill_tripped) *underfill_tripped = tripped;
306+ if (tripped) {
307+ LOG_WARNING << " [EMB-NMC] TemplateBuilder UNDERFILL: selected "
308+ << selected_txs.size () << " tx / " << selected_bytes
309+ << " B into template while mempool holds " << pool.size ()
310+ << " tx / " << mempool_bytes << " B (" << mempool_fees
311+ << " sat fees) — near-empty aux block on a non-empty "
312+ << " mempool; template-fill regression, gates cutover." ;
313+ }
314+ }
315+
248316 // Build GBT-compatible JSON
249317 nlohmann::json data;
250318 data[" version" ] = static_cast <int >(block_version);
0 commit comments