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
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
53 changes: 48 additions & 5 deletions src/impl/dgb/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,33 @@
#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 @@ -184,11 +207,26 @@ 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.
Expand All @@ -209,6 +247,11 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
tmpl["curtime"] = curtime;
tmpl["mintime"] = mintime;
tmpl["transactions"] = nlohmann::json::array();

// previousblockhash: truthful conditional emit (see field notes above).
if (auto th = chain_.tip_hash())
tmpl["previousblockhash"] = u256_be_display_hex(*th);

return tmpl;
}

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));
}

31 changes: 31 additions & 0 deletions src/impl/dgb/test/work_source_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,35 @@ TEST(DgbWorkSource, CoinbaseValueHonorsGbtVerbatim)
kGbt);
}


// previousblockhash: emitted as GBT-conventional big-endian display hex ONLY
// when the HeaderChain carries a real tip hash (tip_hash() accessor). With a
// known tip block_hash, the template surfaces it MSB-limb-first; bits stays
// HELD (no faithful embedded V36 next-target -- MultiShield V4 is V37).
TEST(DgbWorkSource, WorkTemplateEmitsPreviousBlockHashWhenTipCarriesHash)
{
Fixture fx;
fx.chain.set_base_height(400000);
// Seed one Scrypt header carrying a distinctive block id. n_version with
// algo nibble 0x0000 is the Scrypt lane; target 100 with pow_hash 0 (<=
// target) clears the context-free PoW gate; empty-chain MTP is unconstrained.
c2pool::dgb::HeaderSample h;
h.n_version = 0x20000000;
h.n_time = 1000;
h.target = 100;
h.block_hash = dgb::coin::u256::from_u64(0x123456789abcdef0ULL);
ASSERT_EQ(fx.chain.validate_and_append(h),
c2pool::dgb::IngestResult::VALIDATED_SCRYPT);

auto ws = fx.make();
auto tmpl = ws->get_current_work_template();
ASSERT_TRUE(tmpl.contains("previousblockhash"));
// limb[0] is least-significant -> renders as the trailing 16 hex digits,
// preceded by 48 zero digits (limbs 3..1 are zero). 64 chars total.
EXPECT_EQ(tmpl["previousblockhash"].get<std::string>(),
std::string(48, '0') + "123456789abcdef0");
// bits remains deliberately absent (V37 MultiShield V4 wall).
EXPECT_FALSE(tmpl.contains("bits"));
}

} // namespace
Loading