From 008ba081c897eaa9b77c2f3c9d72349208d448e6 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 22:49:35 +0000 Subject: [PATCH] dgb: wire embedded P2P tx relay into the in-process Mempool Add c2pool::dgb::wire_mempool_ingest (coin/mempool_ingest.hpp): subscribe a Mempool to dgb::interfaces::Node::new_tx so the embedded coin P2P tx relay (ADD_P2P_HANDLER(tx)) populates the pool the embedded work template selects from. Until now new_tx fired into the void and the embedded mempool stayed empty regardless of P2P traffic, so an injected EmbeddedTxSource would have nothing to select and transactions[] stayed empty. Tx analog of wire_header_ingest (new_headers -> HeaderChain): the connector adds no policy, delegating txid/dedup/weight/byte-cap to Mempool::add_tx (the insertion SSOT). add_tx is called without a UTXOViewCache, so fees stay fee_known=false until a UTXO view feeds them -- matching the conservative shaper default (fee=null, excluded from the coinbasevalue fold) so a P2P-fed mempool cannot desync coinbasevalue vs daemon GBT. Non-consensus: this only opens the feed; it is not yet called from main_dgb.cpp and the served template is unchanged. Wiring it plus the make_mempool_tx_source drain into the run loop is the follow-up surface-for-tap slice. dgb_mempool_ingest_test 5/5 (links the full dgb_coin codec; in BOTH build.yml --target allowlists, #143). --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/mempool_ingest.hpp | 64 +++++++++++ src/impl/dgb/test/CMakeLists.txt | 14 +++ src/impl/dgb/test/mempool_ingest_test.cpp | 125 ++++++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/mempool_ingest.hpp create mode 100644 src/impl/dgb/test/mempool_ingest_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d67605b21..243100541 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test \ + test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ @@ -200,7 +200,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test \ + test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ diff --git a/src/impl/dgb/coin/mempool_ingest.hpp b/src/impl/dgb/coin/mempool_ingest.hpp new file mode 100644 index 000000000..ef42ad7c1 --- /dev/null +++ b/src/impl/dgb/coin/mempool_ingest.hpp @@ -0,0 +1,64 @@ +#pragma once +// =========================================================================== +// c2pool::dgb::wire_mempool_ingest -- connect the embedded P2P transaction +// relay to the in-process Mempool so live `tx` messages populate the pool the +// embedded work template selects from. +// +// The embedded coin P2P layer (coin/p2p_node.hpp, ADD_P2P_HANDLER(tx)) parses +// each received `tx` message into a coin::Transaction and fires +// dgb::interfaces::Node::new_tx (coin/node_interface.hpp). Until this slice +// nothing consumed that event for the Mempool: relayed transactions were +// dropped on the floor, so the embedded mempool stayed empty regardless of P2P +// traffic and EmbeddedCoinNode's injected EmbeddedTxSource +// (make_mempool_tx_source) had nothing to select -- transactions[] in the +// served template was always empty and coinbasevalue carried subsidy only. +// +// wire_mempool_ingest subscribes the pool to that feed. Each announced +// transaction is converted to the Mempool's MutableTransaction form and handed +// to Mempool::add_tx, which is the single insertion SSOT: it computes the txid, +// rejects duplicates, weighs the tx (BIP141) and enforces the byte cap. This +// connector adds NO policy of its own -- it is the tx analog of +// wire_header_ingest (coin/header_ingest.hpp) for the new_headers feed. +// +// FEE POSTURE: add_tx is called WITHOUT a UTXOViewCache here, so fees stay +// fee_known=false until a UTXO view feeds them. That is deliberate and matches +// the embedded shaper's conservative default (make_mempool_tx_source emits +// fee=null and excludes unknown-fee value from the coinbasevalue fold), so a +// P2P-fed mempool cannot desync coinbasevalue versus a daemon's GBT. Wiring a +// real UTXO view is a later slice; this one only opens the feed. +// +// LIFETIME: the handler captures `pool` by reference, so `pool` MUST outlive +// `node`. The returned EventDisposable lets a caller tear the subscription down +// explicitly; while it (and the node) live, every new_tx relay is ingested. +// =========================================================================== + +#include + +#include + +#include "node_interface.hpp" // dgb::interfaces::Node (new_tx feed) +#include "mempool.hpp" // dgb::coin::Mempool / MutableTransaction +#include "transaction.hpp" // dgb::coin::Transaction -> MutableTransaction + +namespace c2pool::dgb +{ + +// Subscribe `pool` to `node.new_tx`. Returns the subscription handle so the +// caller controls teardown; the subscription persists for the node's life if +// the handle is dropped (EventDisposable does not auto-dispose on destruction). +// +// NOTE: pulls mempool.hpp -> transaction.hpp (the tx serialization codec), so +// include this header ONLY from a TU that already links the full dgb_coin +// codec (main_dgb.cpp + the codec conformance test), never a guard-weight TU +// -- the #143 btclibs SCC trap, identical to embedded_tx_select.hpp. +inline std::shared_ptr +wire_mempool_ingest(::dgb::interfaces::Node& node, ::dgb::coin::Mempool& pool) +{ + return node.new_tx.subscribe( + [&pool](const ::dgb::coin::Transaction& tx) + { + pool.add_tx(::dgb::coin::MutableTransaction(tx)); + }); +} + +} // namespace c2pool::dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 396b99d14..5acfb3cf1 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -65,6 +65,20 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_header_ingest_test "" AUTO) + # dgb_mempool_ingest_test: guards c2pool::dgb::wire_mempool_ingest, the + # connector routing the embedded P2P `tx` relay (new_tx) into the Mempool + # (tx analog of wire_header_ingest). Compiles the tx serialization codec, so + # it links the full dgb_coin SCC set rather than the scrypt-only guard set. + # MUST also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap). + add_executable(dgb_mempool_ingest_test mempool_ingest_test.cpp) + target_link_libraries(dgb_mempool_ingest_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb dgb_stratum + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain + nlohmann_json::nlohmann_json) + gtest_add_tests(dgb_mempool_ingest_test "" AUTO) + # --- #82: won-block reconstructor BODY (as_block composition) ---------- # Pins coin/reconstruct_won_block.hpp: the single composition the dispatch # handler injects as its WonBlockReconstructor (resolve_other_tx_hashes -> diff --git a/src/impl/dgb/test/mempool_ingest_test.cpp b/src/impl/dgb/test/mempool_ingest_test.cpp new file mode 100644 index 000000000..93f8c7454 --- /dev/null +++ b/src/impl/dgb/test/mempool_ingest_test.cpp @@ -0,0 +1,125 @@ +// --------------------------------------------------------------------------- +// dgb_mempool_ingest_test -- pins c2pool::dgb::wire_mempool_ingest, the +// connector that routes the embedded P2P `tx` relay (dgb::interfaces::Node:: +// new_tx) into the in-process Mempool so the embedded work template selects +// from a live pool instead of an always-empty one. +// +// This is the tx analog of dgb_header_ingest_test (new_headers -> HeaderChain). +// Disposition (txid compute, duplicate rejection, weight + byte cap) is +// delegated to Mempool::add_tx, the insertion SSOT; the connector adds no +// policy of its own, so the assertions here pin the WIRING (the feed reaches +// the pool, an unwired node feeds nothing, the handle drives it) rather than +// re-testing the mempool's internals. +// +// Links the full dgb_coin codec like dgb_embedded_tx_select_test (it compiles +// the tx serialization). MUST be in BOTH build.yml --target allowlists (#143 +// NOT_BUILT trap). +// --------------------------------------------------------------------------- + +#include + +#include + +#include +#include +#include +#include + +using c2pool::dgb::wire_mempool_ingest; +using dgb::coin::Mempool; +using dgb::coin::MutableTransaction; +using dgb::coin::Transaction; +using dgb::coin::TxIn; +using dgb::coin::TxOut; +using dgb::coin::compute_txid; + +namespace { + +// A minimal, distinct tx tagged by its prevout index so each has a distinct +// txid (mirrors the builder in dgb_embedded_tx_select_test). +MutableTransaction tagged_tx(uint32_t index) +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = index; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + TxOut out; + out.value = 50'000; + tx.vout.push_back(out); + return tx; +} + +} // namespace + +// 1. A fresh, wired-but-quiet node leaves the pool empty. +TEST(MempoolIngest, EmptyPoolBeforeRelay) +{ + Mempool pool; + dgb::interfaces::Node node; + auto sub = wire_mempool_ingest(node, pool); + + EXPECT_EQ(pool.size(), 0u); +} + +// 2. A tx announced on new_tx is ingested through add_tx: the pool grows and +// the tx is queryable by its txid. +TEST(MempoolIngest, AnnouncedTxIsIngested) +{ + Mempool pool; + dgb::interfaces::Node node; + auto sub = wire_mempool_ingest(node, pool); + + auto mt = tagged_tx(0); + node.new_tx.happened(Transaction(mt)); + + EXPECT_EQ(pool.size(), 1u); + EXPECT_TRUE(pool.contains(compute_txid(mt))); +} + +// 3. Multiple distinct relays accumulate in the pool. +TEST(MempoolIngest, DistinctRelaysAccumulate) +{ + Mempool pool; + dgb::interfaces::Node node; + auto sub = wire_mempool_ingest(node, pool); + + auto a = tagged_tx(0); + auto b = tagged_tx(1); + node.new_tx.happened(Transaction(a)); + node.new_tx.happened(Transaction(b)); + + EXPECT_EQ(pool.size(), 2u); + EXPECT_TRUE(pool.contains(compute_txid(a))); + EXPECT_TRUE(pool.contains(compute_txid(b))); +} + +// 4. Disposition is delegated to add_tx, not the connector: a duplicate relay +// of the same tx is rejected by the pool's SSOT, leaving size unchanged. +TEST(MempoolIngest, DuplicateRelayIsRejected) +{ + Mempool pool; + dgb::interfaces::Node node; + auto sub = wire_mempool_ingest(node, pool); + + auto mt = tagged_tx(0); + node.new_tx.happened(Transaction(mt)); + node.new_tx.happened(Transaction(mt)); // same txid + + EXPECT_EQ(pool.size(), 1u); +} + +// 5. The connector is the driver: a node with NO ingest subscription drops the +// relay -- the pool stays empty. +TEST(MempoolIngest, UnwiredNodeIngestsNothing) +{ + Mempool pool; + dgb::interfaces::Node node; // deliberately NOT wired + + node.new_tx.happened(Transaction(tagged_tx(0))); + + EXPECT_EQ(pool.size(), 0u); +}