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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
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 \
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 \
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 dgb_embedded_tx_select_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \
v37_test \
Expand Down Expand Up @@ -202,7 +202,7 @@ jobs:
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 \
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 \
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 dgb_embedded_tx_select_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
Expand Down
3 changes: 3 additions & 0 deletions src/impl/dgb/coin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ set(dgb_coin_interface
header_chain.hpp
mempool.hpp
template_builder.hpp
embedded_coinbase_value.hpp
embedded_coin_node.hpp
embedded_tx_select.hpp embedded_tx_select.cpp
)

add_library(dgb_coin ${dgb_coin_interface} ${dgb_coin_impl})
Expand Down
59 changes: 51 additions & 8 deletions src/impl/dgb/coin/embedded_coin_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
// The external-daemon GBT fallback (NodeRPC::getwork) is a SEPARATE path
// and is untouched -- this node is the no-external-daemon source only.
// * version pins the Scrypt lane (build_work_template, DGB_BLOCK_VERSION_SCRYPT).
// * transactions[] stays empty and total_fees is 0: embedded mempool tx
// selection is not wired yet, so the template fabricates no transactions
// (the same truthful-absence discipline the stratum caller keeps).
// * transactions[] + the fee total come from an injected EmbeddedTxSource
// (default empty -> empty transactions[], 0 fees, byte-identical to the
// pre-tx-select call site). The production source (embedded_tx_select.cpp,
// make_mempool_tx_source) selects fee-sorted mempool txs; their fee total
// is folded into coinbasevalue via resolve_coinbase_value (#207), never
// fabricated, and transactions[] is passed through build_work_template
// verbatim.
// * previousblockhash is emitted ONLY when HeaderChain::tip_hash() carries a
// real tip id, rendered through the coin/hash_format.hpp SSOT so it is
// byte-identical to the stratum caller's previousblockhash.
Expand All @@ -39,6 +43,8 @@
#include <ctime>
#include <cstdint>
#include <optional>
#include <functional>
#include <utility>

#include <core/pow.hpp> // core::SubsidyFunc

Expand All @@ -51,12 +57,37 @@
namespace dgb::coin
{

// Caller-supplied embedded transaction selection. Decouples EmbeddedCoinNode
// from the heavy tx/UTXO codec (mempool.hpp -> transaction.hpp pulls the
// btclibs serialization templates -- the #143 SCC trap): the node consumes
// only the ALREADY-SHAPED GBT transactions[] array + the fee total, both plain
// JSON/integer, so this header stays codec-free and a guard-weight TU that
// reaches it never compiles the serialization templates. The production shaper
// lives out-of-line in embedded_tx_select.cpp (make_mempool_tx_source).
struct EmbeddedTxSelection
{
// GBT transactions[] array, per-tx {data,txid,hash,fee} -- build_work_template
// passes it through verbatim. Empty array = no txs selected.
nlohmann::json transactions = nlohmann::json::array();
// Sum of the known fees across the selected txs, folded into coinbasevalue
// via the #207 resolve_coinbase_value SSOT (NOT added to the template here).
uint64_t total_fees = 0;
};

// Injected transaction source. An empty std::function (the default) means NO
// transaction source is wired: the node emits an empty transactions[] and 0
// fees -- byte-identical to the pre-tx-select #237 call site. The production
// wiring injects make_mempool_tx_source(mempool, max_weight).
using EmbeddedTxSource = std::function<EmbeddedTxSelection()>;

class EmbeddedCoinNode : public CoinNodeInterface
{
public:
EmbeddedCoinNode(c2pool::dgb::HeaderChain& chain,
core::SubsidyFunc subsidy_func)
: m_chain(chain), m_subsidy_func(std::move(subsidy_func))
core::SubsidyFunc subsidy_func,
EmbeddedTxSource tx_source = {})
: m_chain(chain), m_subsidy_func(std::move(subsidy_func)),
m_tx_source(std::move(tx_source))
{
}

Expand All @@ -67,15 +98,26 @@ class EmbeddedCoinNode : public CoinNodeInterface
{
const uint32_t height = m_chain.next_block_height();

// Pull the caller-supplied transaction selection (empty when no source
// is wired -> byte-identical to the pre-tx-select template). The heavy
// codec that shapes these lives out-of-line (embedded_tx_select.cpp);
// this node only folds the fee total into coinbasevalue and passes the
// transactions[] array through build_work_template.
EmbeddedTxSelection sel;
if (m_tx_source)
sel = m_tx_source();

WorkTemplateInputs in;
in.next_height = height;
// Embedded path: no external GBT value -> derive subsidy(height)+fees.
// total_fees is 0 until embedded mempool tx selection is wired.
// Embedded path: no external GBT value -> derive subsidy(height)+fees
// via the #207 SSOT. total_fees comes from the selected txs (0 when no
// source is wired).
in.coinbasevalue = resolve_coinbase_value(m_subsidy_func, height,
/*total_fees=*/0,
/*total_fees=*/sel.total_fees,
/*gbt_coinbasevalue=*/std::nullopt);
in.median_time_past = m_chain.median_time_past();
in.curtime = curtime;
in.transactions = std::move(sel.transactions);
if (auto th = m_chain.tip_hash())
in.previousblockhash = u256_be_display_hex(*th);
return in;
Expand Down Expand Up @@ -107,6 +149,7 @@ class EmbeddedCoinNode : public CoinNodeInterface
private:
c2pool::dgb::HeaderChain& m_chain;
core::SubsidyFunc m_subsidy_func;
EmbeddedTxSource m_tx_source;
};

} // namespace dgb::coin
64 changes: 64 additions & 0 deletions src/impl/dgb/coin/embedded_tx_select.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// ===========================================================================
// embedded_tx_select.cpp -- out-of-line shaper for the embedded tx source.
//
// The tx/UTXO serialization codec (TX_WITH_WITNESS pack + Hash + HexStr) lives
// here, NOT in any header, so embedded_coin_node.hpp stays codec-free and the
// #143 btclibs SCC trap stays shut. Compiled into dgb_coin (which already links
// the full btclibs serialization), so the codec resolves at link time.
//
// Mirrors the per-tx GBT entry shape src/impl/btc/coin/template_builder.hpp
// emits, so c2pool-dgb and the p2pool-dgb-scrypt reference consume an identical
// transactions[] form (p2pool reads `fee` in helper.py:123 and adjusts subsidy
// in data.py:876-884 when a tx is excluded from the share).
// ===========================================================================

#include "embedded_tx_select.hpp"

#include "transaction.hpp" // TX_WITH_WITNESS, compute_txid is in mempool.hpp

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

#include <string>
#include <utility>

namespace dgb::coin
{

EmbeddedTxSource make_mempool_tx_source(Mempool& pool, uint32_t max_weight)
{
return [&pool, max_weight]() -> 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();
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));
const std::string hex_data = HexStr(packed.get_span());
const uint256 wtxid = Hash(packed.get_span());

nlohmann::json entry;
entry["data"] = hex_data;
entry["txid"] = txid.GetHex();
entry["hash"] = wtxid.GetHex();
if (stx.fee_known)
entry["fee"] = static_cast<int64_t>(stx.fee);
else
entry["fee"] = nullptr; // JSON null -> p2pool base_subsidy fallback
tx_array.push_back(std::move(entry));
}

out.transactions = std::move(tx_array);
return out;
};
}

} // namespace dgb::coin
40 changes: 40 additions & 0 deletions src/impl/dgb/coin/embedded_tx_select.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once
// ===========================================================================
// embedded_tx_select.hpp -- production EmbeddedTxSource factory.
//
// Builds the dgb::coin::EmbeddedTxSource that EmbeddedCoinNode injects to fill
// the embedded work template's transactions[] from the in-process Mempool. The
// actual shaping (Mempool::SelectedTx -> {data,txid,hash,fee}) is the heavy
// tx/UTXO serialization codec, so it lives OUT-OF-LINE in embedded_tx_select.cpp
// (compiled into dgb_coin) -- NOT in this header and NOT in embedded_coin_node.hpp.
// Keeping the codec in a .cpp is what holds the #143 btclibs SCC trap shut: a
// guard-weight TU that reaches embedded_coin_node.hpp never compiles the
// transaction serialization templates.
//
// This header DOES pull mempool.hpp (-> transaction.hpp), so include it ONLY
// from TUs that already link the full dgb_coin codec (main_dgb.cpp + the codec
// conformance test), never from a guard-weight TU.
// ===========================================================================

#include <cstdint>

#include "embedded_coin_node.hpp" // EmbeddedTxSource / EmbeddedTxSelection
#include "mempool.hpp" // dgb::coin::Mempool

namespace dgb::coin
{

// Build an EmbeddedTxSource that, on each invocation, selects up to max_weight
// BIP141 weight units of fee-sorted transactions from `pool` and shapes them
// into the GBT transactions[] form build_work_template passes through verbatim:
// {data: hex(TX_WITH_WITNESS), txid: txid, hash: wtxid, fee: <int|null>}
// The returned selection's total_fees is the sum of the known fees across the
// selected txs (the SAME figure get_sorted_txs_with_fees reports), folded into
// coinbasevalue by EmbeddedCoinNode via resolve_coinbase_value (#207 SSOT) --
// NOT added to the template here.
//
// `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);

} // namespace dgb::coin
15 changes: 15 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ if (BUILD_TESTING AND GTest_FOUND)
nlohmann_json::nlohmann_json)
gtest_add_tests(dgb_embedded_coin_node_test "" AUTO)

# dgb_embedded_tx_select_test: guards make_mempool_tx_source
# (embedded_tx_select.cpp), the production Mempool -> GBT transactions[]
# {data,txid,hash,fee} + fee-total shaper EmbeddedCoinNode injects. Compiles
# the tx serialization codec, so it links the same proven SCC set as
# dgb_embedded_coin_node_test. MUST also be in BOTH build.yml --target
# allowlists (#143 NOT_BUILT trap).
add_executable(dgb_embedded_tx_select_test embedded_tx_select_test.cpp)
target_link_libraries(dgb_embedded_tx_select_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_embedded_tx_select_test "" AUTO)


foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test)
add_executable(${dgb_guard} ${dgb_guard}.cpp)
Expand Down
41 changes: 41 additions & 0 deletions src/impl/dgb/test/embedded_coin_node_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,44 @@ TEST(DgbEmbeddedCoinNode, GetworkProducesTemplateWithNoFabrication)
EXPECT_TRUE(wd.m_hashes.empty()); // no tx hashes fabricated
EXPECT_FALSE(node.is_synced()); // truthful: not synced yet
}

// 5. Injected tx source: the fee total folds into coinbasevalue via the #207
// SSOT and transactions[] passes through build_work_template verbatim.
// Codec-free: a stub source supplies an ALREADY-SHAPED selection, exactly
// as make_mempool_tx_source would, so this guard never compiles the tx
// serialization codec (the embedded_tx_select.cpp / dgb_embedded_tx_select_test
// pins the real shaping over a Mempool).
TEST(DgbEmbeddedCoinNode, InjectedTxSourceFoldsFeesAndPassesTransactions)
{
HeaderChain hc;
hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100});

nlohmann::json txs = nlohmann::json::array();
nlohmann::json e1; e1["data"]="00"; e1["txid"]="aa"; e1["hash"]="aa"; e1["fee"]=700;
nlohmann::json e2; e2["data"]="01"; e2["txid"]="bb"; e2["hash"]="bb"; e2["fee"]=300;
txs.push_back(e1); txs.push_back(e2);
const uint64_t fees = 1000;

EmbeddedCoinNode node(hc, make_subsidy(),
[txs, fees]() -> dgb::coin::EmbeddedTxSelection { return {txs, fees}; });

const uint32_t h = hc.next_block_height();
const nlohmann::json t = build_work_template(node.make_inputs(7));
EXPECT_EQ(t["coinbasevalue"].get<uint64_t>(), 1000ull + h + fees); // subsidy(h)+fees
EXPECT_EQ(t["transactions"], txs); // verbatim pass-through
EXPECT_EQ(t["transactions"].size(), 2u);
}

// 6. Empty (default) tx source == byte-identical to the no-source template
// (the #237 call-site invariant: a node with no tx source fabricates nothing).
TEST(DgbEmbeddedCoinNode, EmptyTxSourceByteIdenticalToNoSource)
{
HeaderChain hc;
hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100});

EmbeddedCoinNode no_src(hc, make_subsidy());
EmbeddedCoinNode empty_src(hc, make_subsidy(), dgb::coin::EmbeddedTxSource{});

EXPECT_EQ(build_work_template(empty_src.make_inputs(5)).dump(),
build_work_template(no_src.make_inputs(5)).dump());
}
Loading
Loading