Skip to content

Commit d67ca9d

Browse files
committed
dgb: extract work-template assembly into build_work_template SSOT (Stage 4c)
Lift the GBT-shaped template assembly out of DGBWorkSource::get_current_work_template() into a free function dgb::coin::build_work_template() in coin/template_builder.hpp, so the stratum work source and the embedded path emit ONE template object and cannot diverge. Pure non-consensus shaping: the consensus-bearing coinbasevalue is still resolved by the caller through the #207 resolve_coinbase_value -> subsidy_func SSOT and passed in verbatim; the builder never recomputes or scales it. version (Scrypt lane pin via DGB_BLOCK_VERSION_SCRYPT), mintime (MTP+1, 0 on empty chain), curtime, empty transactions[] and the truthful conditional previousblockhash all move into the builder unchanged. bits stays held back (MultiShield V4 next-target is V37). Behaviour-preserving: dgb_work_source_test 17/17 unchanged (incl the prevhash getter == template field joint-absence assertion). New guard-weight dgb_template_builder_test 7/7 pins the invariants (Scrypt pin, coinbasevalue pass-through, mintime/empty-chain, empty transactions + no bits, prevhash conditional, determinism). Wired into ctest + both build.yml allowlists (#143 NOT_BUILT trap avoided).
1 parent a36257d commit d67ca9d

5 files changed

Lines changed: 226 additions & 23 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
test_address_resolution test_compute_share_target \
7070
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \
7171
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
72-
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test \
72+
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test \
7373
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7474
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \
7575
v37_test \
@@ -202,7 +202,7 @@ jobs:
202202
test_address_resolution test_compute_share_target \
203203
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \
204204
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
205-
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test \
205+
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test \
206206
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
207207
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \
208208
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \

src/impl/dgb/coin/template_builder.hpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
// matching config_coin.hpp (namespace dgb) and the btc::coin / ltc::coin
1717
// pattern the family-1 seam binds against.
1818

19+
#include <cstdint>
20+
#include <limits>
21+
#include <optional>
1922
#include <stdexcept>
23+
#include <string>
2024

2125
#include <nlohmann/json.hpp>
2226

2327
#include "rpc_data.hpp"
28+
#include "dgb_block_algo.hpp" // DGB_BLOCK_VERSION_SCRYPT (Scrypt lane pin)
2429

2530
namespace dgb
2631
{
@@ -55,6 +60,78 @@ class CoinNodeInterface {
5560
// verbatim from btc once coin/block.hpp is ported.
5661
};
5762

63+
64+
// ── Work-template assembly SSOT (Stage 4c) ──────────────────────────────────
65+
// build_work_template() shapes the already-resolved field values into the
66+
// GBT-compatible JSON template that DGBWorkSource::get_current_work_template()
67+
// returns. Lifting the assembly here makes the stratum work source and the
68+
// embedded path emit ONE template object -- they cannot diverge once both call
69+
// this SSOT (the same intent as routing get_current_gbt_prevhash through
70+
// tip_hash() in Stage 4b).
71+
//
72+
// NON-CONSENSUS: this function only SHAPES values; it never derives or alters
73+
// the consensus-bearing coinbasevalue. That value is computed by the caller
74+
// through the #207 resolve_coinbase_value -> subsidy_func SSOT and passed in
75+
// verbatim. The builder fabricates nothing: transactions[] stays empty
76+
// (embedded mempool tx selection is not wired, fees stay 0), previousblockhash
77+
// is emitted ONLY when the caller supplies a real tip hash (truthful absence,
78+
// never a fabricated id), and `bits` is held back entirely -- DGB Core's live
79+
// next-target is MultiShield V4 (a global 5-algo window == V37), so a
80+
// Scrypt-only walk would emit a known-wrong difficulty (the same fabrication
81+
// the empty transactions[] deliberately avoids). bits becomes a GBT
82+
// pass-through once the external-daemon path is plumbed in.
83+
struct WorkTemplateInputs {
84+
// Absolute height of the NEXT block (#209 next_block_height()).
85+
uint32_t next_height = 0;
86+
// Reward for the next block, already resolved via the #207 SSOT. Passed in
87+
// verbatim; the builder never recomputes or scales it.
88+
uint64_t coinbasevalue = 0;
89+
// DGB Core ContextualCheckBlockHeader lower bound source (median-time-past).
90+
// INT64_MIN means an empty chain (unconstrained) -> mintime emits 0.
91+
int64_t median_time_past = std::numeric_limits<int64_t>::min();
92+
// GBT suggested header nTime. Injected by the caller (work source: wall
93+
// clock) so the assembly is deterministically testable.
94+
int64_t curtime = 0;
95+
// Tip block id as GBT big-endian display hex, already formatted by the
96+
// caller (work source: u256_be_display_hex). nullopt -> previousblockhash
97+
// omitted from the template.
98+
std::optional<std::string> previousblockhash;
99+
};
100+
101+
inline nlohmann::json build_work_template(const WorkTemplateInputs& in)
102+
{
103+
// version: BIP9 base | DGB Scrypt algo nibble (dgb_block_algo.hpp SSOT). A
104+
// DGB template MUST pin the Scrypt lane -- the mining algo lives in 4
105+
// nVersion bits and Scrypt is the all-zero codepoint (DGB_BLOCK_VERSION_SCRYPT
106+
// == 0x0000); any other nibble is a non-Scrypt algo this V36 binary never
107+
// emits a template for.
108+
static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u;
109+
const uint32_t version =
110+
BIP9_BASE_VERSION |
111+
static_cast<uint32_t>(DGB_BLOCK_VERSION_SCRYPT);
112+
113+
// mintime: median_time_past()+1 (DGB Core's nTime > MTP lower bound). An
114+
// empty chain reports INT64_MIN (unconstrained) -> emit 0.
115+
const int64_t mintime =
116+
(in.median_time_past == std::numeric_limits<int64_t>::min())
117+
? 0 : (in.median_time_past + 1);
118+
119+
nlohmann::json tmpl = nlohmann::json::object();
120+
tmpl["height"] = in.next_height;
121+
tmpl["coinbasevalue"] = in.coinbasevalue;
122+
tmpl["version"] = version;
123+
tmpl["curtime"] = in.curtime;
124+
tmpl["mintime"] = mintime;
125+
tmpl["transactions"] = nlohmann::json::array();
126+
127+
// previousblockhash: truthful conditional emit (see struct notes).
128+
if (in.previousblockhash)
129+
tmpl["previousblockhash"] = *in.previousblockhash;
130+
131+
return tmpl;
132+
}
133+
134+
58135
} // namespace coin
59136

60137
} // namespace dgb

src/impl/dgb/stratum/work_source.cpp

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <impl/dgb/coin/mempool.hpp>
2020
#include <impl/dgb/coin/embedded_coinbase_value.hpp>
2121
#include <impl/dgb/coin/dgb_block_algo.hpp>
22+
#include <impl/dgb/coin/template_builder.hpp>
2223

2324
#include <core/log.hpp>
2425

@@ -239,29 +240,21 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
239240
// and the per-connection coinbase (gentx + ShareTracker ref_hash + PPLNS
240241
// payout map) assembles in build_connection_coinbase() — that output is
241242
// consensus-bearing and surfaces for an operator tap, not in this field wire.
242-
static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u;
243-
const uint32_t version =
244-
BIP9_BASE_VERSION |
245-
static_cast<uint32_t>(dgb::coin::DGB_BLOCK_VERSION_SCRYPT);
246-
247-
const int64_t mtp = chain_.median_time_past();
248-
const int64_t mintime = (mtp == std::numeric_limits<int64_t>::min())
249-
? 0 : (mtp + 1);
250-
const int64_t curtime = static_cast<int64_t>(std::time(nullptr));
251-
252-
nlohmann::json tmpl = nlohmann::json::object();
253-
tmpl["height"] = next_h;
254-
tmpl["coinbasevalue"] = coinbasevalue;
255-
tmpl["version"] = version;
256-
tmpl["curtime"] = curtime;
257-
tmpl["mintime"] = mintime;
258-
tmpl["transactions"] = nlohmann::json::array();
259-
260-
// previousblockhash: truthful conditional emit (see field notes above).
243+
// Shape the truthfully-derivable fields into the GBT template via the
244+
// dgb::coin::build_work_template SSOT so the embedded path and this work
245+
// source emit one template (Stage 4c extraction). version (Scrypt lane
246+
// pin), mintime (MTP+1 / 0 on empty chain), curtime, empty transactions[]
247+
// and the conditional previousblockhash all live in the builder now; this
248+
// method only resolves the chain-state inputs.
249+
dgb::coin::WorkTemplateInputs in;
250+
in.next_height = next_h;
251+
in.coinbasevalue = coinbasevalue;
252+
in.median_time_past = chain_.median_time_past();
253+
in.curtime = static_cast<int64_t>(std::time(nullptr));
261254
if (auto th = chain_.tip_hash())
262-
tmpl["previousblockhash"] = u256_be_display_hex(*th);
255+
in.previousblockhash = u256_be_display_hex(*th);
263256

264-
return tmpl;
257+
return dgb::coin::build_work_template(in);
265258
}
266259

267260
std::vector<std::string> DGBWorkSource::get_stratum_merkle_branches() const

src/impl/dgb/test/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ if (BUILD_TESTING AND GTest_FOUND)
142142
nlohmann_json::nlohmann_json)
143143
gtest_add_tests(dgb_work_source_test "" AUTO)
144144

145+
# dgb_template_builder_test: guards the build_work_template SSOT
146+
# (Stage 4c extraction). Pure JSON-shaping function -> guard-weight
147+
# linkage: the only out-of-line dep reachable through template_builder.hpp
148+
# -> rpc_data.hpp -> core/uint256.hpp is declarations only (no uint256
149+
# method is invoked), so GTest + nlohmann_json suffice. MUST also be in the build.yml
150+
# --target allowlist (#143 NOT_BUILT trap).
151+
add_executable(dgb_template_builder_test template_builder_test.cpp)
152+
target_link_libraries(dgb_template_builder_test PRIVATE
153+
GTest::gtest_main GTest::gtest
154+
nlohmann_json::nlohmann_json)
155+
gtest_add_tests(dgb_template_builder_test "" AUTO)
156+
145157

146158
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test)
147159
add_executable(${dgb_guard} ${dgb_guard}.cpp)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// dgb_template_builder_test — guards the dgb::coin::build_work_template SSOT
2+
// (Stage 4c extraction). The work-template assembly was lifted out of
3+
// DGBWorkSource::get_current_work_template() into coin/template_builder.hpp so
4+
// the stratum work source and the embedded path emit ONE template object and
5+
// cannot diverge. These tests pin the truthfulness invariants the assembly
6+
// must hold: the Scrypt lane is pinned, the consensus coinbasevalue passes
7+
// through verbatim, mintime tracks median-time-past (0 on an empty chain),
8+
// transactions[] stays empty, bits is never emitted, and previousblockhash is
9+
// a truthful conditional (present only when a real tip hash is supplied).
10+
//
11+
// Pure-shaping function -> guard-weight test (no chain/mempool fixture).
12+
13+
#include <cstdint>
14+
#include <limits>
15+
#include <optional>
16+
#include <string>
17+
18+
#include <gtest/gtest.h>
19+
#include <nlohmann/json.hpp>
20+
21+
#include <impl/dgb/coin/template_builder.hpp>
22+
#include <impl/dgb/coin/dgb_block_algo.hpp>
23+
24+
using dgb::coin::WorkTemplateInputs;
25+
using dgb::coin::build_work_template;
26+
27+
namespace {
28+
29+
// A fully-populated set of inputs mirroring a mid-chain template build.
30+
WorkTemplateInputs make_inputs()
31+
{
32+
WorkTemplateInputs in;
33+
in.next_height = 17722;
34+
in.coinbasevalue = 625000000ULL; // arbitrary post-resolve reward
35+
in.median_time_past = 1750000000; // a concrete MTP
36+
in.curtime = 1750000075; // MTP + ~one block period
37+
in.previousblockhash =
38+
"00000000000000000a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778";
39+
return in;
40+
}
41+
42+
} // namespace
43+
44+
// version MUST be BIP9 base OR'd with the all-zero Scrypt algo nibble. A DGB
45+
// template that is not Scrypt-pinned would be a block this V36 binary never mines.
46+
TEST(DgbTemplateBuilder, ScryptLanePinnedVersion)
47+
{
48+
auto t = build_work_template(make_inputs());
49+
static constexpr uint32_t BIP9_BASE = 0x20000000u;
50+
const uint32_t expect =
51+
BIP9_BASE | static_cast<uint32_t>(dgb::coin::DGB_BLOCK_VERSION_SCRYPT);
52+
EXPECT_EQ(t.at("version").get<uint32_t>(), expect);
53+
EXPECT_EQ(expect, 0x20000000u); // Scrypt == all-zero codepoint
54+
// The version nibble must classify back to Scrypt through the SSOT.
55+
EXPECT_TRUE(dgb::coin::is_scrypt_header(t.at("version").get<int32_t>()));
56+
}
57+
58+
// The consensus-bearing reward is resolved upstream (#207 SSOT) and must pass
59+
// through the builder byte-for-byte — the builder never recomputes or scales it.
60+
TEST(DgbTemplateBuilder, CoinbaseValuePassesThroughVerbatim)
61+
{
62+
auto in = make_inputs();
63+
in.coinbasevalue = 0xCAFEBABEDEADBEEFULL;
64+
auto t = build_work_template(in);
65+
EXPECT_EQ(t.at("coinbasevalue").get<uint64_t>(), 0xCAFEBABEDEADBEEFULL);
66+
EXPECT_EQ(t.at("height").get<uint32_t>(), in.next_height);
67+
}
68+
69+
// mintime = median_time_past + 1 (DGB Core's nTime > MTP lower bound).
70+
TEST(DgbTemplateBuilder, MintimeIsMtpPlusOne)
71+
{
72+
auto in = make_inputs();
73+
in.median_time_past = 1750000000;
74+
auto t = build_work_template(in);
75+
EXPECT_EQ(t.at("mintime").get<int64_t>(), 1750000001);
76+
EXPECT_EQ(t.at("curtime").get<int64_t>(), in.curtime);
77+
}
78+
79+
// Empty chain reports MTP == INT64_MIN (unconstrained) -> mintime emits 0,
80+
// never INT64_MIN+1 (which would underflow-adjacent and be meaningless to GBT).
81+
TEST(DgbTemplateBuilder, EmptyChainMintimeIsZero)
82+
{
83+
auto in = make_inputs();
84+
in.median_time_past = std::numeric_limits<int64_t>::min();
85+
auto t = build_work_template(in);
86+
EXPECT_EQ(t.at("mintime").get<int64_t>(), 0);
87+
}
88+
89+
// transactions[] is always an empty array (embedded tx selection unwired);
90+
// bits is NEVER emitted (MultiShield V4 next-target is V37 — a Scrypt-only
91+
// walk would be a known-wrong difficulty).
92+
TEST(DgbTemplateBuilder, EmptyTransactionsAndNoBits)
93+
{
94+
auto t = build_work_template(make_inputs());
95+
ASSERT_TRUE(t.at("transactions").is_array());
96+
EXPECT_TRUE(t.at("transactions").empty());
97+
EXPECT_FALSE(t.contains("bits"));
98+
}
99+
100+
// previousblockhash is a truthful conditional: emitted verbatim when a real
101+
// tip hash is supplied, omitted entirely when absent (never a fabricated id).
102+
TEST(DgbTemplateBuilder, PreviousblockhashConditionalEmit)
103+
{
104+
auto with = build_work_template(make_inputs());
105+
ASSERT_TRUE(with.contains("previousblockhash"));
106+
EXPECT_EQ(with.at("previousblockhash").get<std::string>(),
107+
make_inputs().previousblockhash.value());
108+
109+
auto in = make_inputs();
110+
in.previousblockhash = std::nullopt;
111+
auto without = build_work_template(in);
112+
EXPECT_FALSE(without.contains("previousblockhash"));
113+
}
114+
115+
// Divergence guard: identical inputs -> byte-identical template. This is the
116+
// whole point of the SSOT — the work source and embedded path cannot diverge.
117+
TEST(DgbTemplateBuilder, DeterministicForIdenticalInputs)
118+
{
119+
EXPECT_EQ(build_work_template(make_inputs()).dump(),
120+
build_work_template(make_inputs()).dump());
121+
}

0 commit comments

Comments
 (0)