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_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_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_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_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_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_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
26 changes: 22 additions & 4 deletions src/impl/dgb/coin/header_chain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ using ::dgb::coin::u256;
// == 0 is the default for "scrypt(header) not evaluated here" (trivially
// satisfies any target); the daemon port fills it.
struct HeaderSample {
int32_t n_version = 0;
int64_t n_time = 0;
u256 target = 0; // expanded PoW target (smaller == more work)
u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW
int32_t n_version = 0;
int64_t n_time = 0;
u256 target = 0; // expanded PoW target (smaller == more work)
u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW
u256 block_hash = 0; // sha256d(header) block id; 0 == not populated here
};

// Outcome of validating + ingesting one header.
Expand Down Expand Up @@ -358,6 +359,23 @@ class HeaderChain {
return static_cast<uint32_t>(m_base_height + (m_chain.size() - 1));
}

// Block hash of the newest header, or nullopt when the chain is empty OR
// the tip carries no hash. DGB's block id is sha256d over the 80-byte
// header (params.hpp block_hash_func == sha256d) -- distinct from pow_hash,
// which is the scrypt(header) PoW digest. HeaderSample stores it as a u256
// and the work-template emitter renders the GBT-conventional big-endian
// display hex. block_hash == 0 is the "not populated here" sentinel (the
// SAME convention pow_hash uses): the embedded-daemon header-ingest port
// fills it at the validate_and_append boundary, so until that lands this
// returns nullopt and get_current_work_template holds previousblockhash
// back -- a truthful absence, never a fabricated hash.
std::optional<u256> tip_hash() const
{
if (m_chain.empty() || m_chain.back().block_hash.is_zero())
return std::nullopt;
return m_chain.back().block_hash;
}

// Absolute height of the block the next template builds on top of the tip
// == tip_height()+1, or m_base_height for an empty chain. This is the
// template builder's `next_h` (btc template_builder.hpp: tip.height + 1).
Expand Down
77 changes: 77 additions & 0 deletions src/impl/dgb/coin/template_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
// matching config_coin.hpp (namespace dgb) and the btc::coin / ltc::coin
// pattern the family-1 seam binds against.

#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>

#include <nlohmann/json.hpp>

#include "rpc_data.hpp"
#include "dgb_block_algo.hpp" // DGB_BLOCK_VERSION_SCRYPT (Scrypt lane pin)

namespace dgb
{
Expand Down Expand Up @@ -55,6 +60,78 @@ class CoinNodeInterface {
// verbatim from btc once coin/block.hpp is ported.
};


// ── Work-template assembly SSOT (Stage 4c) ──────────────────────────────────
// build_work_template() shapes the already-resolved field values into the
// GBT-compatible JSON template that DGBWorkSource::get_current_work_template()
// returns. Lifting the assembly here makes the stratum work source and the
// embedded path emit ONE template object -- they cannot diverge once both call
// this SSOT (the same intent as routing get_current_gbt_prevhash through
// tip_hash() in Stage 4b).
//
// NON-CONSENSUS: this function only SHAPES values; it never derives or alters
// the consensus-bearing coinbasevalue. That value is computed by the caller
// through the #207 resolve_coinbase_value -> subsidy_func SSOT and passed in
// verbatim. The builder fabricates nothing: transactions[] stays empty
// (embedded mempool tx selection is not wired, fees stay 0), previousblockhash
// is emitted ONLY when the caller supplies a real tip hash (truthful absence,
// never a fabricated id), and `bits` is held back entirely -- DGB Core's live
// next-target is MultiShield V4 (a global 5-algo window == V37), so a
// Scrypt-only walk would emit a known-wrong difficulty (the same fabrication
// the empty transactions[] deliberately avoids). bits becomes a GBT
// pass-through once the external-daemon path is plumbed in.
struct WorkTemplateInputs {
// Absolute height of the NEXT block (#209 next_block_height()).
uint32_t next_height = 0;
// Reward for the next block, already resolved via the #207 SSOT. Passed in
// verbatim; the builder never recomputes or scales it.
uint64_t coinbasevalue = 0;
// DGB Core ContextualCheckBlockHeader lower bound source (median-time-past).
// INT64_MIN means an empty chain (unconstrained) -> mintime emits 0.
int64_t median_time_past = std::numeric_limits<int64_t>::min();
// GBT suggested header nTime. Injected by the caller (work source: wall
// clock) so the assembly is deterministically testable.
int64_t curtime = 0;
// Tip block id as GBT big-endian display hex, already formatted by the
// caller (work source: u256_be_display_hex). nullopt -> previousblockhash
// omitted from the template.
std::optional<std::string> previousblockhash;
};

inline nlohmann::json build_work_template(const WorkTemplateInputs& in)
{
// version: BIP9 base | DGB Scrypt algo nibble (dgb_block_algo.hpp SSOT). A
// DGB template MUST pin the Scrypt lane -- the mining algo lives in 4
// nVersion bits and Scrypt is the all-zero codepoint (DGB_BLOCK_VERSION_SCRYPT
// == 0x0000); any other nibble is a non-Scrypt algo this V36 binary never
// emits a template for.
static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u;
const uint32_t version =
BIP9_BASE_VERSION |
static_cast<uint32_t>(DGB_BLOCK_VERSION_SCRYPT);

// mintime: median_time_past()+1 (DGB Core's nTime > MTP lower bound). An
// empty chain reports INT64_MIN (unconstrained) -> emit 0.
const int64_t mintime =
(in.median_time_past == std::numeric_limits<int64_t>::min())
? 0 : (in.median_time_past + 1);

nlohmann::json tmpl = nlohmann::json::object();
tmpl["height"] = in.next_height;
tmpl["coinbasevalue"] = in.coinbasevalue;
tmpl["version"] = version;
tmpl["curtime"] = in.curtime;
tmpl["mintime"] = mintime;
tmpl["transactions"] = nlohmann::json::array();

// previousblockhash: truthful conditional emit (see struct notes).
if (in.previousblockhash)
tmpl["previousblockhash"] = *in.previousblockhash;

return tmpl;
}


} // namespace coin

} // namespace dgb
93 changes: 69 additions & 24 deletions src/impl/dgb/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,38 @@
#include <impl/dgb/coin/mempool.hpp>
#include <impl/dgb/coin/embedded_coinbase_value.hpp>
#include <impl/dgb/coin/dgb_block_algo.hpp>
#include <impl/dgb/coin/template_builder.hpp>

#include <core/log.hpp>

#include <ctime>
#include <string>
#include <limits>

namespace dgb::stratum {

namespace {

// Render a u256 as the GBT-conventional big-endian block-hash display hex:
// most-significant limb first, 64 lowercase hex digits, no 0x prefix. Mirrors
// uint256::GetHex() ordering for a hash stored with limb[0] least-significant.
// Header-only u256 has no GetHex(), and this TU must not depend on btclibs'
// uint256, so we format the limbs directly.
std::string u256_be_display_hex(const dgb::coin::u256& v)
{
static constexpr char H[] = "0123456789abcdef";
std::string out;
out.reserve(64);
for (int li = 3; li >= 0; --li) {
const uint64_t w = v.limb[li];
for (int sh = 60; sh >= 0; sh -= 4)
out.push_back(H[(w >> sh) & 0xF]);
}
return out;
}

} // namespace

DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain,
dgb::coin::Mempool& mempool,
bool is_testnet,
Expand Down Expand Up @@ -84,7 +108,16 @@ std::function<uint256()> DGBWorkSource::get_best_share_hash_fn() const

std::string DGBWorkSource::get_current_gbt_prevhash() const
{
// Stage 4b: read chain_.tip() and return BE-display-hex form.
// The tip block id as GBT-conventional big-endian display hex, drawn from
// the SAME source as get_current_work_template()'s previousblockhash field
// (chain_.tip_hash(), the #216 HeaderChain accessor) through the SAME
// u256_be_display_hex formatter -- ONE truthful source, so the dedicated
// getter and the assembled template can never silently diverge. Empty
// string when the chain carries no real tip hash (tip_hash() == nullopt:
// an empty chain, or the block_hash==0 sentinel from the not-yet-wired
// embedded P2P header ingest) -- a truthful absence, never a fabricated id.
if (auto th = chain_.tip_hash())
return u256_be_display_hex(*th);
return {};
}

Expand Down Expand Up @@ -184,32 +217,44 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
// so no transactions are fabricated and fees stay 0
// (consistent with the total_fees=0 coinbasevalue above).
//
// Deliberately NOT emitted yet — they need accessors the Scrypt-only
// HeaderSample does not carry, and land in the following Stage 4c/4d slices:
// previousblockhash — the tip block hash (HeaderSample stores no hash yet)
// bits — the next-block compact target off the DigiShield
// retarget window
// previousblockhash — the tip block id. Emitted ONLY when the HeaderChain
// carries a real tip hash (tip_hash() accessor): the
// Scrypt-only HeaderSample now carries a block_hash slot,
// but the embedded P2P header-download -> validate_and_append
// ingest that POPULATES it lands in a following slice, so on
// today's chain state tip_hash() is nullopt and the field is
// held back -- a truthful absence, never a fabricated hash.
// bits — HELD BACK. The only embedded next-target source is the
// DigiShield/MultiShield damped multiply, which DGB Core
// runs as MultiShield V4: a GLOBAL window across all 5 algos
// with per-algo adjust + MTP deltas. A Scrypt-only header
// walk cannot reconstruct that window (== V37, 5-algo
// validation), so the ingest path deliberately demotes the
// retarget gate to a no-op (see header_chain.hpp). Emitting
// a digishield_next_target()-derived bits would be a
// KNOWN-WRONG value -- the same fabrication the empty
// transactions[] and total_fees=0 avoid. The authoritative
// bits is the external-daemon GBT value, which is not
// plumbed into this embedded template path yet; bits stays
// absent until then. [decision-needed] surfaced to integrator.
// and the per-connection coinbase (gentx + ShareTracker ref_hash + PPLNS
// payout map) assembles in build_connection_coinbase() — that output is
// consensus-bearing and surfaces for an operator tap, not in this field wire.
static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u;
const uint32_t version =
BIP9_BASE_VERSION |
static_cast<uint32_t>(dgb::coin::DGB_BLOCK_VERSION_SCRYPT);

const int64_t mtp = chain_.median_time_past();
const int64_t mintime = (mtp == std::numeric_limits<int64_t>::min())
? 0 : (mtp + 1);
const int64_t curtime = static_cast<int64_t>(std::time(nullptr));

nlohmann::json tmpl = nlohmann::json::object();
tmpl["height"] = next_h;
tmpl["coinbasevalue"] = coinbasevalue;
tmpl["version"] = version;
tmpl["curtime"] = curtime;
tmpl["mintime"] = mintime;
tmpl["transactions"] = nlohmann::json::array();
return tmpl;
// Shape the truthfully-derivable fields into the GBT template via the
// dgb::coin::build_work_template SSOT so the embedded path and this work
// source emit one template (Stage 4c extraction). version (Scrypt lane
// pin), mintime (MTP+1 / 0 on empty chain), curtime, empty transactions[]
// and the conditional previousblockhash all live in the builder now; this
// method only resolves the chain-state inputs.
dgb::coin::WorkTemplateInputs in;
in.next_height = next_h;
in.coinbasevalue = coinbasevalue;
in.median_time_past = chain_.median_time_past();
in.curtime = static_cast<int64_t>(std::time(nullptr));
if (auto th = chain_.tip_hash())
in.previousblockhash = u256_be_display_hex(*th);

return dgb::coin::build_work_template(in);
}

std::vector<std::string> DGBWorkSource::get_stratum_merkle_branches() const
Expand Down
12 changes: 12 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ if (BUILD_TESTING AND GTest_FOUND)
nlohmann_json::nlohmann_json)
gtest_add_tests(dgb_work_source_test "" AUTO)

# dgb_template_builder_test: guards the build_work_template SSOT
# (Stage 4c extraction). Pure JSON-shaping function -> guard-weight
# linkage: the only out-of-line dep reachable through template_builder.hpp
# -> rpc_data.hpp -> core/uint256.hpp is declarations only (no uint256
# method is invoked), so GTest + nlohmann_json suffice. MUST also be in the build.yml
# --target allowlist (#143 NOT_BUILT trap).
add_executable(dgb_template_builder_test template_builder_test.cpp)
target_link_libraries(dgb_template_builder_test PRIVATE
GTest::gtest_main GTest::gtest
nlohmann_json::nlohmann_json)
gtest_add_tests(dgb_template_builder_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
58 changes: 58 additions & 0 deletions src/impl/dgb/test/header_chain_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -727,3 +727,61 @@ TEST(HeaderChainBlockHeight, CheckpointSeedNumbersFirstHeader)
EXPECT_EQ(hc.tip_height().value(), 12345u);
EXPECT_EQ(hc.next_block_height(), 12346u);
}

// ─────────────────────────────────────────────────────────────────────────────
// tip_hash(): the Scrypt-only HeaderSample now carries a sha256d block-id slot,
// surfaced for the work template's previousblockhash. block_hash == 0 is the
// "not populated here" sentinel (same convention as pow_hash), so the accessor
// reports absence rather than a fabricated all-zero hash.
// ─────────────────────────────────────────────────────────────────────────────

TEST(HeaderChainTipHash, EmptyChainHasNoTipHash)
{
HeaderChain hc;
EXPECT_FALSE(hc.tip_hash().has_value());
}

TEST(HeaderChainTipHash, UnpopulatedHashStaysNullopt)
{
// A header validated/appended WITHOUT a block_hash (the chain-helper path,
// exactly as every other test here) leaves block_hash == 0 -> nullopt, NOT
// a zero hash. Guards against previousblockhash being emitted as all-zeros.
HeaderChain hc;
ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}),
IngestResult::VALIDATED_SCRYPT);
EXPECT_TRUE(hc.tip_height().has_value()); // height advanced...
EXPECT_FALSE(hc.tip_hash().has_value()); // ...but no hash carried.
}

TEST(HeaderChainTipHash, ReturnsNewestPopulatedHash)
{
HeaderChain hc;
HeaderSample h1{SCRYPT, 1000, 100};
h1.block_hash = dgb::coin::u256::from_u64(0x1111ull);
ASSERT_EQ(hc.validate_and_append(h1), IngestResult::VALIDATED_SCRYPT);
ASSERT_TRUE(hc.tip_hash().has_value());
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x1111ull));

// A newer header's hash supersedes the prior tip's.
HeaderSample h2{SCRYPT, 1100, 100};
h2.block_hash = dgb::coin::u256::from_u64(0x2222ull);
ASSERT_EQ(hc.validate_and_append(h2), IngestResult::VALIDATED_SCRYPT);
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x2222ull));
}

TEST(HeaderChainTipHash, ContinuityHeaderHashIsTip)
{
// A non-Scrypt continuity header still extends the chain and becomes the
// tip, so its block_hash (if carried) is the tip hash -- previousblockhash
// tracks the actual newest block id regardless of algo.
HeaderChain hc;
HeaderSample s1{SCRYPT, 1000, 100};
s1.block_hash = dgb::coin::u256::from_u64(0xaaaaull);
ASSERT_EQ(hc.validate_and_append(s1), IngestResult::VALIDATED_SCRYPT);

HeaderSample c1{SHA256D, 1075, 999999};
c1.block_hash = dgb::coin::u256::from_u64(0xbbbbull);
ASSERT_EQ(hc.validate_and_append(c1), IngestResult::ACCEPTED_CONTINUITY);
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0xbbbbull));
}

Loading
Loading