Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
cmake --build build_ci \
--target test_hardening test_share_messages test_coin_broadcaster \
test_redistribute_address test_redistribute test_auto_ratchet \
test_stratum_extensions test_stratum_hotel_interim \
test_stratum_extensions test_stratum_hotel_interim test_btc_underfill_guard test_dgb_underfill_guard \
core_test sharechain_test share_test btc_share_test \
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
Expand Down Expand Up @@ -267,7 +267,7 @@ jobs:
boost_sentinel \
test_hardening test_share_messages \
test_redistribute_address test_redistribute test_auto_ratchet \
test_stratum_extensions test_stratum_hotel_interim \
test_stratum_extensions test_stratum_hotel_interim test_btc_underfill_guard test_dgb_underfill_guard \
core_test sharechain_test share_test btc_share_test \
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
Expand Down Expand Up @@ -394,7 +394,7 @@ jobs:
bch_embedded_block_broadcast_test bch_coin_node_seam_test \
bch_asert_kat_test \
bch_muldiv_kat_test \
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test test_bch_underfill_guard \
-j8

- name: Run BCH tests
Expand Down Expand Up @@ -496,7 +496,7 @@ jobs:
bch_embedded_block_broadcast_test bch_coin_node_seam_test \
bch_asert_kat_test \
bch_muldiv_kat_test \
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test test_bch_underfill_guard \
-j8

- name: Run BCH tests under sanitizers
Expand Down
67 changes: 66 additions & 1 deletion src/impl/bch/coin/template_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,38 @@ inline std::string bits_to_hex(uint32_t bits) {
return std::string(buf);
}

// ── Underfill guard (v36 cutover deploy path) ───────────────────────────────
// Port of the LTC/DOGE template-builder guard (src/impl/ltc/coin/
// template_builder.hpp, src/impl/doge/coin/template_builder.hpp) to the BCH
// embedded template path, in the DASH free-predicate form
// (src/impl/dash/coin/embedded_gbt.hpp). Detects the "near-empty template on a
// non-empty mempool" regression: the tx selector returns almost no
// transactions even though the local mempool holds a substantial fee-paying
// backlog. c2pool-side template-fill safety net — NOT the byte-parity KAT
// axis; thresholds are the v36-native shared structure (standardize
// cross-coin) pinned to the legacy p2pool near-empty floor (~50 kB),
// identical to LTC/DOGE/DASH. BCH serializes txs in a single canonical form
// (no segwit), so "bytes" here are the same serialized-tx bytes the mempool
// byte cap and the ABLA byte budget count.
inline constexpr uint64_t UNDERFILL_MIN_FILL_BYTES = 50'000ull; // < this = near-empty block
inline constexpr uint64_t UNDERFILL_BACKLOG_SLACK = 50'000ull; // unselected fee-paying material that should have filled it

/// Pure trip predicate — the exact boolean the LTC/DOGE guards evaluate.
/// Factored out so the KAT can pin it without a log scraper:
/// near_empty : template packed fewer bytes than the near-empty floor
/// has_backlog : the mempool holds fee-paying material (known fees > 0)
/// well beyond what was selected (> selected + slack)
/// Genuinely empty (or fee-unknown-only) mempools never trip.
inline bool underfill_guard_trips(uint64_t selected_bytes,
uint64_t mempool_bytes,
uint64_t mempool_known_fees)
{
const bool near_empty = selected_bytes < UNDERFILL_MIN_FILL_BYTES;
const bool has_backlog = mempool_known_fees > 0
&& mempool_bytes > selected_bytes + UNDERFILL_BACKLOG_SLACK;
return near_empty && has_backlog;
}

// ─── TemplateBuilder ─────────────────────────────────────────────────────────

/// Builds a BCH block template (WorkData) from a validated HeaderChain and
Expand All @@ -126,11 +158,17 @@ class TemplateBuilder {
// sourced per-network from abla.hpp (see build_template / file head).
static constexpr uint32_t COINBASE_RESERVE = 1'000u; // bytes for coinbase

/// underfill_tripped: optional underfill-guard observation seam. Defaults
/// to nullptr so every existing caller is byte-for-byte unchanged
/// (SAFE-ADDITIVE); the guard KAT passes a bool to pin the wiring without
/// a log scraper. The guard itself is log-only (WARNING), exactly like
/// LTC/DOGE — it never alters the template.
static std::optional<rpc::WorkData> build_template(
const HeaderChain& chain,
const Mempool& pool,
bool is_testnet = false,
const abla::State* tip_state = nullptr)
const abla::State* tip_state = nullptr,
bool* underfill_tripped = nullptr)
{
auto t0 = std::chrono::steady_clock::now();

Expand Down Expand Up @@ -202,9 +240,11 @@ class TemplateBuilder {
std::vector<Transaction> tx_objects;
std::vector<uint256> tx_hashes;

uint64_t selected_bytes = 0; // wire bytes packed into this template (underfill guard)
for (const auto& stx : selected_txs) {
uint256 txid = compute_txid(stx.tx);
auto packed = pack(stx.tx); // single canonical form
selected_bytes += packed.get_span().size();
std::string hex_data = HexStr(packed.get_span());

nlohmann::json entry;
Expand All @@ -223,6 +263,31 @@ class TemplateBuilder {
tx_hashes.push_back(txid);
}

// ── Underfill guard ────────────────────────────────────────────────
// Do not silently treat a near-empty template as healthy when the
// mempool held fee-paying backlog that should have filled it. We cannot
// fabricate transactions, so we surface loudly (WARNING) for
// contabo-prod-watch / the operator rather than shipping a false-empty
// block as normal. Genuinely empty mempools never trip this. Mirrors
// the LTC/DOGE TemplateBuilder guard; additive only — CTOR ordering
// above and the GBT JSON below are untouched either way.
{
const uint64_t mempool_bytes = static_cast<uint64_t>(pool.byte_size());
const uint64_t mempool_fees = pool.total_fees();
const bool tripped = underfill_guard_trips(selected_bytes,
mempool_bytes,
mempool_fees);
if (underfill_tripped) *underfill_tripped = tripped;
if (tripped) {
LOG_WARNING << "[EMB-BCH] TemplateBuilder UNDERFILL: selected "
<< selected_txs.size() << " tx / " << selected_bytes
<< "B into template while mempool holds " << pool.size()
<< " tx / " << mempool_bytes << "B (" << mempool_fees
<< " sat fees) — near-empty block on a non-empty "
<< "mempool; template-fill regression, gates cutover.";
}
}

// ── GBT-compatible JSON ────────────────────────────────────────────
nlohmann::json data;
data["version"] = static_cast<int>(block_version);
Expand Down
65 changes: 64 additions & 1 deletion src/impl/btc/coin/template_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,36 @@ inline std::string bits_to_hex(uint32_t bits) {
return std::string(buf);
}

// ── Underfill guard (v36 cutover deploy path) ───────────────────────────────
// Port of the LTC/DOGE template-builder guard (src/impl/ltc/coin/
// template_builder.hpp, src/impl/doge/coin/template_builder.hpp) to the BTC
// embedded template path, in the DASH free-predicate form
// (src/impl/dash/coin/embedded_gbt.hpp). Detects the "near-empty template on a
// non-empty mempool" regression: the tx selector returns almost no
// transactions even though the local mempool holds a substantial fee-paying
// backlog. c2pool-side template-fill safety net — NOT the byte-parity KAT
// axis; thresholds are the v36-native shared structure (standardize
// cross-coin) pinned to the legacy p2pool near-empty floor (~50 kB),
// identical to LTC/DOGE/DASH.
inline constexpr uint64_t UNDERFILL_MIN_FILL_BYTES = 50'000ull; // < this = near-empty block
inline constexpr uint64_t UNDERFILL_BACKLOG_SLACK = 50'000ull; // unselected fee-paying material that should have filled it

/// Pure trip predicate — the exact boolean the LTC/DOGE guards evaluate.
/// Factored out so the KAT can pin it without a log scraper:
/// near_empty : template packed fewer bytes than the near-empty floor
/// has_backlog : the mempool holds fee-paying material (known fees > 0)
/// well beyond what was selected (> selected + slack)
/// Genuinely empty (or fee-unknown-only) mempools never trip.
inline bool underfill_guard_trips(uint64_t selected_bytes,
uint64_t mempool_bytes,
uint64_t mempool_known_fees)
{
const bool near_empty = selected_bytes < UNDERFILL_MIN_FILL_BYTES;
const bool has_backlog = mempool_known_fees > 0
&& mempool_bytes > selected_bytes + UNDERFILL_BACKLOG_SLACK;
return near_empty && has_backlog;
}

// ─── TemplateBuilder ─────────────────────────────────────────────────────────

/// Builds an LTC block template from a validated HeaderChain and Mempool.
Expand Down Expand Up @@ -218,10 +248,16 @@ class TemplateBuilder {

/// Build a WorkData template from the current chain tip + mempool.
/// Returns std::nullopt if the chain has no tip yet (not synced to genesis).
/// underfill_tripped: optional underfill-guard observation seam. Defaults
/// to nullptr so every existing caller is byte-for-byte unchanged
/// (SAFE-ADDITIVE); the guard KAT passes a bool to pin the wiring without
/// a log scraper. The guard itself is log-only (WARNING), exactly like
/// LTC/DOGE — it never alters the template.
static std::optional<rpc::WorkData> build_template(
const HeaderChain& chain,
const Mempool& pool,
bool is_testnet = false)
bool is_testnet = false,
bool* underfill_tripped = nullptr)
{
(void)is_testnet; // reserved for future per-network rules
auto t0 = std::chrono::steady_clock::now();
Expand Down Expand Up @@ -278,9 +314,11 @@ class TemplateBuilder {
std::vector<Transaction> tx_objects;
std::vector<uint256> tx_hashes;

uint64_t selected_bytes = 0; // wire bytes packed into this template (underfill guard)
for (const auto& stx : selected_txs) {
uint256 txid = compute_txid(stx.tx);
auto packed = pack(TX_WITH_WITNESS(stx.tx));
selected_bytes += packed.get_span().size();
std::string hex_data = HexStr(packed.get_span());
// wtxid = SHA256d of witness serialization (for witness merkle tree)
uint256 wtxid = Hash(packed.get_span());
Expand All @@ -302,6 +340,31 @@ class TemplateBuilder {
tx_hashes.push_back(txid);
}

// ── Underfill guard ────────────────────────────────────────────────
// Do not silently treat a near-empty template as healthy when the
// mempool held fee-paying backlog that should have filled it. We cannot
// fabricate transactions, so we surface loudly (WARNING) for
// contabo-prod-watch / the operator rather than shipping a false-empty
// block as normal. Genuinely empty mempools never trip this. Mirrors
// the LTC/DOGE TemplateBuilder guard; additive only — the GBT JSON
// below is untouched either way.
{
const uint64_t mempool_bytes = static_cast<uint64_t>(pool.byte_size());
const uint64_t mempool_fees = pool.total_fees();
const bool tripped = underfill_guard_trips(selected_bytes,
mempool_bytes,
mempool_fees);
if (underfill_tripped) *underfill_tripped = tripped;
if (tripped) {
LOG_WARNING << "[EMB-BTC] TemplateBuilder UNDERFILL: selected "
<< selected_txs.size() << " tx / " << selected_bytes
<< "B into template while mempool holds " << pool.size()
<< " tx / " << mempool_bytes << "B (" << mempool_fees
<< " sat fees) — near-empty block on a non-empty "
<< "mempool; template-fill regression, gates cutover.";
}
}

// ── Build GBT-compatible JSON ──────────────────────────────────────
nlohmann::json data;
data["version"] = static_cast<int>(block_version);
Expand Down
37 changes: 35 additions & 2 deletions src/impl/dgb/coin/embedded_tx_select.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,35 @@

#include <core/pack.hpp> // pack
#include <core/hash.hpp> // Hash (sha256d)
#include <core/log.hpp> // LOG_WARNING (underfill guard)
#include <btclibs/util/strencodings.h> // HexStr

#include <cstdint>
#include <string>
#include <utility>

namespace dgb::coin
{

EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight)
EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight,
bool* underfill_tripped)
{
return [&pool, max_weight]() -> EmbeddedTxSelection {
return [&pool, max_weight, underfill_tripped]() -> EmbeddedTxSelection {
auto [selected, total_fees] = pool.get_sorted_txs_with_fees(max_weight);

EmbeddedTxSelection out;
out.total_fees = total_fees;

nlohmann::json tx_array = nlohmann::json::array();
uint64_t selected_bytes = 0; // wire bytes packed into this template (underfill guard)
for (const auto& stx : selected)
{
// data = with-witness serialization hex (what a daemon submits)
// txid = legacy (non-witness) sha256d
// hash = wtxid (with-witness sha256d) for the witness merkle tree
const uint256 txid = compute_txid(stx.tx);
auto packed = pack(TX_WITH_WITNESS(stx.tx));
selected_bytes += packed.get_span().size();
const std::string hex_data = HexStr(packed.get_span());
const uint256 wtxid = Hash(packed.get_span());

Expand All @@ -57,6 +62,34 @@ EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight)
tx_array.push_back(std::move(entry));
}

// ── Underfill guard ────────────────────────────────────────────────
// Do not silently treat a near-empty selection as healthy when the
// mempool held fee-paying backlog that should have filled it. This is
// the ONE mempool-visible point feeding BOTH build_work_template
// callers (stratum DGBWorkSource + embedded CoinNodeInterface), so the
// guard evaluates here — predicate + thresholds in template_builder.hpp
// (underfill_guard_trips), mirroring LTC/DOGE semantics exactly. We
// cannot fabricate transactions, so surface loudly (WARNING) for the
// operator rather than shipping a false-empty block as normal.
// Genuinely empty mempools never trip. Log-only; the selection above
// is untouched either way.
{
const uint64_t mempool_bytes = static_cast<uint64_t>(pool.byte_size());
const uint64_t mempool_fees = pool.total_fees();
const bool tripped = underfill_guard_trips(selected_bytes,
mempool_bytes,
mempool_fees);
if (underfill_tripped) *underfill_tripped = tripped;
if (tripped) {
LOG_WARNING << "[EMB-DGB] make_mempool_tx_source UNDERFILL: selected "
<< selected.size() << " tx / " << selected_bytes
<< "B into template while mempool holds " << pool.size()
<< " tx / " << mempool_bytes << "B (" << mempool_fees
<< " sat fees) — near-empty block on a non-empty "
<< "mempool; template-fill regression, gates cutover.";
}
}

out.transactions = std::move(tx_array);
return out;
};
Expand Down
12 changes: 11 additions & 1 deletion src/impl/dgb/coin/embedded_tx_select.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ namespace dgb::coin
//
// `pool` is captured by reference -- it MUST outlive the returned source (the
// embedded node and its mempool share a lifetime).
EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight);
//
// underfill_tripped: optional underfill-guard observation seam (see
// template_builder.hpp underfill_guard_trips). Each invocation of the returned
// source evaluates the LTC/DOGE "near-empty template on a non-empty mempool"
// guard over the REAL pool queries (byte_size / total_fees) and, when the
// pointer is non-null, writes the trip boolean through it so the KAT can pin
// the wiring without a log scraper. Defaults to nullptr so every existing
// caller is byte-for-byte unchanged (SAFE-ADDITIVE); the guard itself is
// log-only (WARNING) and never alters the selection.
EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight,
bool* underfill_tripped = nullptr);

} // namespace dgb::coin
Loading
Loading