Skip to content

Commit 8a8b0bf

Browse files
authored
dgb: tip_hash() accessor + previousblockhash wire (Stage 4c) (#216)
dgb: tip_hash() accessor + previousblockhash wire (Stage 4c)
2 parents e2d90ec + 30a6d78 commit 8a8b0bf

4 files changed

Lines changed: 159 additions & 9 deletions

File tree

src/impl/dgb/coin/header_chain.hpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ using ::dgb::coin::u256;
7575
// == 0 is the default for "scrypt(header) not evaluated here" (trivially
7676
// satisfies any target); the daemon port fills it.
7777
struct HeaderSample {
78-
int32_t n_version = 0;
79-
int64_t n_time = 0;
80-
u256 target = 0; // expanded PoW target (smaller == more work)
81-
u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW
78+
int32_t n_version = 0;
79+
int64_t n_time = 0;
80+
u256 target = 0; // expanded PoW target (smaller == more work)
81+
u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW
82+
u256 block_hash = 0; // sha256d(header) block id; 0 == not populated here
8283
};
8384

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

362+
// Block hash of the newest header, or nullopt when the chain is empty OR
363+
// the tip carries no hash. DGB's block id is sha256d over the 80-byte
364+
// header (params.hpp block_hash_func == sha256d) -- distinct from pow_hash,
365+
// which is the scrypt(header) PoW digest. HeaderSample stores it as a u256
366+
// and the work-template emitter renders the GBT-conventional big-endian
367+
// display hex. block_hash == 0 is the "not populated here" sentinel (the
368+
// SAME convention pow_hash uses): the embedded-daemon header-ingest port
369+
// fills it at the validate_and_append boundary, so until that lands this
370+
// returns nullopt and get_current_work_template holds previousblockhash
371+
// back -- a truthful absence, never a fabricated hash.
372+
std::optional<u256> tip_hash() const
373+
{
374+
if (m_chain.empty() || m_chain.back().block_hash.is_zero())
375+
return std::nullopt;
376+
return m_chain.back().block_hash;
377+
}
378+
361379
// Absolute height of the block the next template builds on top of the tip
362380
// == tip_height()+1, or m_base_height for an empty chain. This is the
363381
// template builder's `next_h` (btc template_builder.hpp: tip.height + 1).

src/impl/dgb/stratum/work_source.cpp

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,33 @@
2323
#include <core/log.hpp>
2424

2525
#include <ctime>
26+
#include <string>
2627
#include <limits>
2728

2829
namespace dgb::stratum {
2930

31+
namespace {
32+
33+
// Render a u256 as the GBT-conventional big-endian block-hash display hex:
34+
// most-significant limb first, 64 lowercase hex digits, no 0x prefix. Mirrors
35+
// uint256::GetHex() ordering for a hash stored with limb[0] least-significant.
36+
// Header-only u256 has no GetHex(), and this TU must not depend on btclibs'
37+
// uint256, so we format the limbs directly.
38+
std::string u256_be_display_hex(const dgb::coin::u256& v)
39+
{
40+
static constexpr char H[] = "0123456789abcdef";
41+
std::string out;
42+
out.reserve(64);
43+
for (int li = 3; li >= 0; --li) {
44+
const uint64_t w = v.limb[li];
45+
for (int sh = 60; sh >= 0; sh -= 4)
46+
out.push_back(H[(w >> sh) & 0xF]);
47+
}
48+
return out;
49+
}
50+
51+
} // namespace
52+
3053
DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain,
3154
dgb::coin::Mempool& mempool,
3255
bool is_testnet,
@@ -184,11 +207,26 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
184207
// so no transactions are fabricated and fees stay 0
185208
// (consistent with the total_fees=0 coinbasevalue above).
186209
//
187-
// Deliberately NOT emitted yet — they need accessors the Scrypt-only
188-
// HeaderSample does not carry, and land in the following Stage 4c/4d slices:
189-
// previousblockhash — the tip block hash (HeaderSample stores no hash yet)
190-
// bits — the next-block compact target off the DigiShield
191-
// retarget window
210+
// previousblockhash — the tip block id. Emitted ONLY when the HeaderChain
211+
// carries a real tip hash (tip_hash() accessor): the
212+
// Scrypt-only HeaderSample now carries a block_hash slot,
213+
// but the embedded P2P header-download -> validate_and_append
214+
// ingest that POPULATES it lands in a following slice, so on
215+
// today's chain state tip_hash() is nullopt and the field is
216+
// held back -- a truthful absence, never a fabricated hash.
217+
// bits — HELD BACK. The only embedded next-target source is the
218+
// DigiShield/MultiShield damped multiply, which DGB Core
219+
// runs as MultiShield V4: a GLOBAL window across all 5 algos
220+
// with per-algo adjust + MTP deltas. A Scrypt-only header
221+
// walk cannot reconstruct that window (== V37, 5-algo
222+
// validation), so the ingest path deliberately demotes the
223+
// retarget gate to a no-op (see header_chain.hpp). Emitting
224+
// a digishield_next_target()-derived bits would be a
225+
// KNOWN-WRONG value -- the same fabrication the empty
226+
// transactions[] and total_fees=0 avoid. The authoritative
227+
// bits is the external-daemon GBT value, which is not
228+
// plumbed into this embedded template path yet; bits stays
229+
// absent until then. [decision-needed] surfaced to integrator.
192230
// and the per-connection coinbase (gentx + ShareTracker ref_hash + PPLNS
193231
// payout map) assembles in build_connection_coinbase() — that output is
194232
// consensus-bearing and surfaces for an operator tap, not in this field wire.
@@ -209,6 +247,11 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
209247
tmpl["curtime"] = curtime;
210248
tmpl["mintime"] = mintime;
211249
tmpl["transactions"] = nlohmann::json::array();
250+
251+
// previousblockhash: truthful conditional emit (see field notes above).
252+
if (auto th = chain_.tip_hash())
253+
tmpl["previousblockhash"] = u256_be_display_hex(*th);
254+
212255
return tmpl;
213256
}
214257

src/impl/dgb/test/header_chain_test.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,3 +727,61 @@ TEST(HeaderChainBlockHeight, CheckpointSeedNumbersFirstHeader)
727727
EXPECT_EQ(hc.tip_height().value(), 12345u);
728728
EXPECT_EQ(hc.next_block_height(), 12346u);
729729
}
730+
731+
// ─────────────────────────────────────────────────────────────────────────────
732+
// tip_hash(): the Scrypt-only HeaderSample now carries a sha256d block-id slot,
733+
// surfaced for the work template's previousblockhash. block_hash == 0 is the
734+
// "not populated here" sentinel (same convention as pow_hash), so the accessor
735+
// reports absence rather than a fabricated all-zero hash.
736+
// ─────────────────────────────────────────────────────────────────────────────
737+
738+
TEST(HeaderChainTipHash, EmptyChainHasNoTipHash)
739+
{
740+
HeaderChain hc;
741+
EXPECT_FALSE(hc.tip_hash().has_value());
742+
}
743+
744+
TEST(HeaderChainTipHash, UnpopulatedHashStaysNullopt)
745+
{
746+
// A header validated/appended WITHOUT a block_hash (the chain-helper path,
747+
// exactly as every other test here) leaves block_hash == 0 -> nullopt, NOT
748+
// a zero hash. Guards against previousblockhash being emitted as all-zeros.
749+
HeaderChain hc;
750+
ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}),
751+
IngestResult::VALIDATED_SCRYPT);
752+
EXPECT_TRUE(hc.tip_height().has_value()); // height advanced...
753+
EXPECT_FALSE(hc.tip_hash().has_value()); // ...but no hash carried.
754+
}
755+
756+
TEST(HeaderChainTipHash, ReturnsNewestPopulatedHash)
757+
{
758+
HeaderChain hc;
759+
HeaderSample h1{SCRYPT, 1000, 100};
760+
h1.block_hash = dgb::coin::u256::from_u64(0x1111ull);
761+
ASSERT_EQ(hc.validate_and_append(h1), IngestResult::VALIDATED_SCRYPT);
762+
ASSERT_TRUE(hc.tip_hash().has_value());
763+
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x1111ull));
764+
765+
// A newer header's hash supersedes the prior tip's.
766+
HeaderSample h2{SCRYPT, 1100, 100};
767+
h2.block_hash = dgb::coin::u256::from_u64(0x2222ull);
768+
ASSERT_EQ(hc.validate_and_append(h2), IngestResult::VALIDATED_SCRYPT);
769+
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x2222ull));
770+
}
771+
772+
TEST(HeaderChainTipHash, ContinuityHeaderHashIsTip)
773+
{
774+
// A non-Scrypt continuity header still extends the chain and becomes the
775+
// tip, so its block_hash (if carried) is the tip hash -- previousblockhash
776+
// tracks the actual newest block id regardless of algo.
777+
HeaderChain hc;
778+
HeaderSample s1{SCRYPT, 1000, 100};
779+
s1.block_hash = dgb::coin::u256::from_u64(0xaaaaull);
780+
ASSERT_EQ(hc.validate_and_append(s1), IngestResult::VALIDATED_SCRYPT);
781+
782+
HeaderSample c1{SHA256D, 1075, 999999};
783+
c1.block_hash = dgb::coin::u256::from_u64(0xbbbbull);
784+
ASSERT_EQ(hc.validate_and_append(c1), IngestResult::ACCEPTED_CONTINUITY);
785+
EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0xbbbbull));
786+
}
787+

src/impl/dgb/test/work_source_test.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,35 @@ TEST(DgbWorkSource, CoinbaseValueHonorsGbtVerbatim)
282282
kGbt);
283283
}
284284

285+
286+
// previousblockhash: emitted as GBT-conventional big-endian display hex ONLY
287+
// when the HeaderChain carries a real tip hash (tip_hash() accessor). With a
288+
// known tip block_hash, the template surfaces it MSB-limb-first; bits stays
289+
// HELD (no faithful embedded V36 next-target -- MultiShield V4 is V37).
290+
TEST(DgbWorkSource, WorkTemplateEmitsPreviousBlockHashWhenTipCarriesHash)
291+
{
292+
Fixture fx;
293+
fx.chain.set_base_height(400000);
294+
// Seed one Scrypt header carrying a distinctive block id. n_version with
295+
// algo nibble 0x0000 is the Scrypt lane; target 100 with pow_hash 0 (<=
296+
// target) clears the context-free PoW gate; empty-chain MTP is unconstrained.
297+
c2pool::dgb::HeaderSample h;
298+
h.n_version = 0x20000000;
299+
h.n_time = 1000;
300+
h.target = 100;
301+
h.block_hash = dgb::coin::u256::from_u64(0x123456789abcdef0ULL);
302+
ASSERT_EQ(fx.chain.validate_and_append(h),
303+
c2pool::dgb::IngestResult::VALIDATED_SCRYPT);
304+
305+
auto ws = fx.make();
306+
auto tmpl = ws->get_current_work_template();
307+
ASSERT_TRUE(tmpl.contains("previousblockhash"));
308+
// limb[0] is least-significant -> renders as the trailing 16 hex digits,
309+
// preceded by 48 zero digits (limbs 3..1 are zero). 64 chars total.
310+
EXPECT_EQ(tmpl["previousblockhash"].get<std::string>(),
311+
std::string(48, '0') + "123456789abcdef0");
312+
// bits remains deliberately absent (V37 MultiShield V4 wall).
313+
EXPECT_FALSE(tmpl.contains("bits"));
314+
}
315+
285316
} // namespace

0 commit comments

Comments
 (0)