-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathwork_source_test.cpp
More file actions
546 lines (496 loc) · 22.5 KB
/
Copy pathwork_source_test.cpp
File metadata and controls
546 lines (496 loc) · 22.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
// dgb::stratum::DGBWorkSource — Stage 4a skeleton construction + contract test.
//
// Proves the work source instantiates against the live coin types
// (c2pool::dgb::HeaderChain + dgb::coin::Mempool), satisfies the full
// core::stratum::IWorkSource pure-virtual contract (so core::StratumServer
// can hold it via shared_ptr<IWorkSource> in the next slice), and that its
// real-now surface (config defaults, atomic work-generation, share-target
// atomics, worker registry, best-share callback) behaves. The stubbed
// work-generation / submit methods are asserted to return their documented
// safe defaults so a regression that accidentally "implements" them with
// garbage is caught.
//
// MUST appear in BOTH this ctest registration AND the build.yml --target
// allowlist, or it becomes a #143-style NOT_BUILT sentinel that reds master.
#include <impl/dgb/stratum/work_source.hpp>
#include <impl/dgb/coin/header_chain.hpp>
#include <impl/dgb/coin/mempool.hpp>
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::subsidy (oracle SSOT)
#include <impl/dgb/params.hpp> // dgb::make_coin_params -- the LIVE production binding
#include <core/pow.hpp> // core::SubsidyFunc
#include <core/stratum_work_source.hpp>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
namespace {
// IDENTICAL to params.hpp `p.subsidy_func` — the live CoinParams indirection.
const core::SubsidyFunc kSubsidyFunc =
[](uint32_t height) -> uint64_t { return dgb::CoinParams::subsidy(height); };
// Construct a DGBWorkSource over default-constructed coin deps. The submit
// callback records whether it was invoked (it must NOT be in the 4a skeleton).
struct Fixture {
c2pool::dgb::HeaderChain chain;
dgb::coin::Mempool mempool;
bool submit_called = false;
std::unique_ptr<dgb::stratum::DGBWorkSource> make()
{
auto fn = [this](const std::vector<unsigned char>&, uint32_t) -> bool {
submit_called = true;
return false;
};
return std::make_unique<dgb::stratum::DGBWorkSource>(
chain, mempool, /*is_testnet=*/false, fn, kSubsidyFunc);
}
};
TEST(DgbWorkSource, ConstructsAndSatisfiesIWorkSourceContract)
{
Fixture fx;
auto ws = fx.make();
// Usable through the abstract interface core::StratumServer holds.
core::stratum::IWorkSource* iface = ws.get();
ASSERT_NE(iface, nullptr);
}
TEST(DgbWorkSource, ConfigDefaultsMatchStratumConfig)
{
Fixture fx;
auto ws = fx.make();
const auto& cfg = ws->get_stratum_config();
EXPECT_DOUBLE_EQ(cfg.min_difficulty, 0.0005);
EXPECT_DOUBLE_EQ(cfg.max_difficulty, 65536.0);
EXPECT_DOUBLE_EQ(cfg.target_time, 3.0);
EXPECT_TRUE(cfg.vardiff_enabled);
}
TEST(DgbWorkSource, WorkGenerationStartsZeroAndBumps)
{
Fixture fx;
auto ws = fx.make();
EXPECT_EQ(ws->get_work_generation(), 0u);
ws->bump_work_generation();
ws->bump_work_generation();
EXPECT_EQ(ws->get_work_generation(), 2u);
}
TEST(DgbWorkSource, ShareTargetAtomicsRoundTrip)
{
Fixture fx;
auto ws = fx.make();
EXPECT_EQ(ws->get_share_bits(), 0u);
EXPECT_EQ(ws->get_share_max_bits(), 0u);
ws->set_share_target(0x1d00ffff, 0x1e0fffff);
EXPECT_EQ(ws->get_share_bits(), 0x1d00ffffu);
EXPECT_EQ(ws->get_share_max_bits(), 0x1e0fffffu);
}
TEST(DgbWorkSource, NoMergedChainInDefaultBuild)
{
Fixture fx;
auto ws = fx.make();
// DGB V36 default build is a standalone Scrypt parent (no merged mining;
// -DAUX_DOGE dual-parent is a parked STRETCH).
EXPECT_FALSE(ws->has_merged_chain(0x0001));
}
TEST(DgbWorkSource, BestShareHashFnEmptyUntilWired)
{
Fixture fx;
auto ws = fx.make();
EXPECT_FALSE(static_cast<bool>(ws->get_best_share_hash_fn()));
ws->set_best_share_hash_fn([]() { return uint256::ZERO; });
auto fn = ws->get_best_share_hash_fn();
ASSERT_TRUE(static_cast<bool>(fn));
EXPECT_EQ(fn(), uint256::ZERO);
}
// ── Worker->mint sharechain-accept seam (set_mint_share_fn / try_mint_share) ──
// The producer half of the worker->mint run-loop standup: DGBWorkSource hands a
// share-difficulty submission's found-share fields to a callback main_dgb.cpp
// binds to mint_local_share_with_ratchet (#294) -> create_local_share. These
// pin the seam contract before the stage-4d classify branch reaches it.
TEST(DgbWorkSource, MintShareFnEmptyUntilWiredReturnsNullNoSilentDrop)
{
Fixture fx;
auto ws = fx.make();
// Unbound: try_mint_share must NOT crash and must return a NULL hash
// (the accepted share is logged, never silently dispatched into a null fn).
dgb::stratum::DGBWorkSource::MintShareInputs in;
in.subsidy = 500000000;
EXPECT_EQ(ws->try_mint_share(in), uint256::ZERO);
}
TEST(DgbWorkSource, MintShareFnForwardsInputsAndReturnsHash)
{
Fixture fx;
auto ws = fx.make();
// Spy mint callback: capture the inputs (forward) and return a sentinel
// hash (pass-through back to the classify branch).
dgb::stratum::DGBWorkSource::MintShareInputs seen;
bool called = false;
uint256 sentinel; sentinel.SetHex(
"00000000000000000000000000000000000000000000000000000000cafe5a7e");
ws->set_mint_share_fn(
[&](const dgb::stratum::DGBWorkSource::MintShareInputs& got) -> uint256 {
called = true;
seen = got;
return sentinel;
});
dgb::stratum::DGBWorkSource::MintShareInputs in;
in.header_bytes = std::vector<unsigned char>(80, 0xab);
in.coinbase_bytes = {0x03, 0x01, 0x02, 0x03};
in.subsidy = 0x1234567890ULL;
in.prev_share.SetHex(
"00000000000000000000000000000000000000000000000000000000000000aa");
in.merkle_branches.push_back(in.prev_share);
in.payout_script = {0x76, 0xa9};
in.segwit_active = true;
uint256 minted = ws->try_mint_share(in);
EXPECT_TRUE(called);
EXPECT_EQ(minted, sentinel); // minted hash flows back verbatim
EXPECT_EQ(seen.header_bytes.size(), 80u); // inputs forwarded, not dropped
EXPECT_EQ(seen.coinbase_bytes.size(), 4u);
EXPECT_EQ(seen.subsidy, 0x1234567890ULL);
EXPECT_EQ(seen.prev_share, in.prev_share);
ASSERT_EQ(seen.merkle_branches.size(), 1u);
EXPECT_EQ(seen.merkle_branches[0], in.prev_share);
EXPECT_EQ(seen.payout_script.size(), 2u);
EXPECT_TRUE(seen.segwit_active);
}
// No behavior change this slice: the seam is stood up but the 4a mining_submit
// stub still rejects every submission and must NOT reach the mint callback
// (the classify branch that calls try_mint_share lands in stage 4d).
TEST(DgbWorkSource, MiningSubmitStubDoesNotInvokeMintFnYet)
{
Fixture fx;
auto ws = fx.make();
bool mint_called = false;
ws->set_mint_share_fn(
[&](const dgb::stratum::DGBWorkSource::MintShareInputs&) -> uint256 {
mint_called = true;
return uint256{};
});
auto result = ws->mining_submit(
"DGBaddr.worker1", "job-0", "en1", "en2", "ntime", "nonce", "rid-0",
/*merged_addresses=*/{}, /*job=*/nullptr);
ASSERT_TRUE(result.is_array());
EXPECT_FALSE(result[0].get<bool>()); // 4a stub still rejects
EXPECT_FALSE(mint_called); // seam wired but NOT yet reached
}
TEST(DgbWorkSource, WorkerRegistryRoundTrip)
{
Fixture fx;
auto ws = fx.make();
core::stratum::WorkerInfo info;
info.username = "DGBaddr.worker1";
info.worker_name = "worker1";
ws->register_stratum_worker("sess-1", info);
ws->update_stratum_worker("sess-1", /*hashrate=*/1.0e9, /*dead=*/0.0,
/*difficulty=*/16.0, /*accepted=*/3, /*rejected=*/0, /*stale=*/0);
// No crash + idempotent unregister of a known + unknown session.
ws->unregister_stratum_worker("sess-1");
ws->unregister_stratum_worker("sess-unknown");
SUCCEED();
}
TEST(DgbWorkSource, WorkGenStubsReturnSafeDefaults)
{
Fixture fx;
auto ws = fx.make();
// 4a skeleton: every work-generation getter returns its documented
// empty/default form (4c fills them in).
EXPECT_TRUE(ws->get_current_gbt_prevhash().empty());
// get_current_work_template() now emits height + coinbasevalue (Stage 4c
// coinbasevalue wire); its dedicated assertions live in
// WorkTemplateEmitsHeightAndCoinbaseValueViaSsot below.
EXPECT_TRUE(ws->get_current_work_template().is_object());
EXPECT_TRUE(ws->get_stratum_merkle_branches().empty());
auto parts = ws->get_coinbase_parts();
EXPECT_TRUE(parts.first.empty());
EXPECT_TRUE(parts.second.empty());
}
// -- Stage-4d mining_submit classify ladder (live invocation point) ----------
// A real JobSnapshot drives reconstruct -> Scrypt digest -> classify_submission
// -> dispatch. The Scrypt PoW of an arbitrary header is not steerable, so each
// KAT pins the OUTCOME CLASS by the TARGETS, not the hash: a genuinely-maximal
// compact target (0x2100ffff -> 0xffff<<240, ~2^256 -- clears every digest, NOT
// the regtest 0x207fffff which is only ~2^255 and rejects MSB-set digests) makes
// WonBlock/ShareAccept deterministic; a near-zero target (0x03000001 -> target 1)
// is satisfied by none (Reject). This exercises
// the exact tighten-first ladder the hot path runs without a scrypt fixture.
namespace {
core::stratum::JobSnapshot make_job(uint32_t share_bits, const std::string& block_nbits)
{
core::stratum::JobSnapshot j;
j.coinb1 = "01000000"; // minimal coinbase head (well-formed hex)
j.coinb2 = "00000000"; // minimal coinbase tail
j.gbt_prevhash = std::string(64, '0'); // 32-byte prevhash, BE display hex
j.nbits = "1e0fffff"; // header (share) bits
j.version = 0x20000000u;
j.share_bits = share_bits;
j.block_nbits = block_nbits;
j.subsidy = 500000000ULL;
j.segwit_active = false;
return j;
}
const char* kEN1 = "00000000";
const char* kEN2 = "00000000";
const char* kNT = "60000000";
const char* kNON = "00000000";
} // namespace
TEST(DgbWorkSource, MiningSubmitWonBlockDispatchesBroadcaster)
{
Fixture fx;
auto ws = fx.make();
// block_nbits = 0x2100ffff (maximal target ~2^256): every Scrypt digest
// clears it -> WonBlock -> submit_block_fn_ MUST fire (dual-path broadcaster, #82).
auto job = make_job(/*share_bits=*/0x2100ffffu, /*block_nbits=*/"2100ffff");
auto result = ws->mining_submit(
"DGBaddr.worker1", "job-won", kEN1, kEN2, kNT, kNON, "rid",
/*merged_addresses=*/{}, &job);
ASSERT_TRUE(result.is_boolean());
EXPECT_TRUE(result.get<bool>()); // won block -> accepted reply
EXPECT_TRUE(fx.submit_called); // broadcaster reached
}
TEST(DgbWorkSource, MiningSubmitShareAcceptDispatchesMint)
{
Fixture fx;
auto ws = fx.make();
bool minted = false;
dgb::stratum::DGBWorkSource::MintShareInputs seen;
ws->set_mint_share_fn(
[&](const dgb::stratum::DGBWorkSource::MintShareInputs& got) -> uint256 {
minted = true; seen = got;
uint256 h; h.SetHex(
"00000000000000000000000000000000000000000000000000000000000b10c5");
return h;
});
// block_nbits = 0x03000001 (target 1: no digest is a block) but share_bits =
// 0x2100ffff (maximal -> any digest clears) -> ShareAccept -> try_mint_share
// fires, the won-block broadcaster does NOT.
auto job = make_job(/*share_bits=*/0x2100ffffu, /*block_nbits=*/"03000001");
auto result = ws->mining_submit(
"DGBaddr.worker1", "job-share", kEN1, kEN2, kNT, kNON, "rid",
/*merged_addresses=*/{}, &job);
ASSERT_TRUE(result.is_boolean());
EXPECT_TRUE(result.get<bool>()); // valid share -> accepted reply
EXPECT_TRUE(minted); // mint dispatch reached
EXPECT_FALSE(fx.submit_called); // NOT a block -> no broadcast
EXPECT_EQ(seen.header_bytes.size(), 80u); // 80-byte header forwarded
EXPECT_EQ(seen.subsidy, 500000000ULL); // subsidy carried from the job
}
TEST(DgbWorkSource, MiningSubmitLowDifficultyRejectsNeitherDispatch)
{
Fixture fx;
auto ws = fx.make();
bool minted = false;
ws->set_mint_share_fn(
[&](const dgb::stratum::DGBWorkSource::MintShareInputs&) -> uint256 {
minted = true; return uint256{};
});
// Both targets near-zero (0x03000001 -> target 1): no Scrypt digest clears
// either -> Reject. Neither the broadcaster nor the mint dispatch fires.
auto job = make_job(/*share_bits=*/0x03000001u, /*block_nbits=*/"03000001");
auto result = ws->mining_submit(
"DGBaddr.worker1", "job-rej", kEN1, kEN2, kNT, kNON, "rid",
/*merged_addresses=*/{}, &job);
ASSERT_TRUE(result.is_array());
EXPECT_FALSE(result[0].get<bool>()); // reject form [false, [code,msg,null]]
EXPECT_FALSE(fx.submit_called);
EXPECT_FALSE(minted);
}
TEST(DgbWorkSource, MiningSubmitStubRejectsWithoutCallingBroadcaster)
{
Fixture fx;
auto ws = fx.make();
auto result = ws->mining_submit(
"DGBaddr.worker1", "job-0", "en1", "en2", "ntime", "nonce", "rid-0",
/*merged_addresses=*/{}, /*job=*/nullptr);
// Stratum mining.submit response = [false, [code, msg, null]] reject form.
ASSERT_TRUE(result.is_array());
ASSERT_GE(result.size(), 1u);
EXPECT_FALSE(result[0].get<bool>());
// The 4a stub must NOT have reached the won-block broadcaster.
EXPECT_FALSE(fx.submit_called);
}
TEST(DgbWorkSource, ComputeShareDifficultyReturnsNotYetSentinel)
{
Fixture fx;
auto ws = fx.make();
// 4a skeleton: the per-coin (Scrypt) PoW-difficulty hook returns the
// documented 0.0 parse-error/not-yet sentinel. The coin-agnostic
// StratumServer's vardiff gate treats 0.0 as a hard reject, so no
// garbage difficulty leaks into the rate monitor before 4b/4c land
// the real scrypt_1024_1_1_256 assembly.
double diff = ws->compute_share_difficulty(
"coinb1", "coinb2", "en1", "en2", "ntime", "nonce",
/*version=*/0x20000000u, "prevhash", "1e0ffff0",
/*merkle_branches=*/{});
EXPECT_DOUBLE_EQ(diff, 0.0);
}
// Stage 4c coinbasevalue wire: the work template surfaces the NEXT-block
// height and its coinbasevalue, the latter derived THROUGH the #207 SSOT
// (subsidy_func) keyed on next_block_height() == tip.height + 1 (#209). An
// empty chain makes next_block_height() == base_height, so seeding an oracle
// era boundary pins the value unambiguously to the p2pool-dgb-scrypt subsidy.
TEST(DgbWorkSource, WorkTemplateEmitsHeightAndCoinbaseValueViaSsot)
{
Fixture fx;
fx.chain.set_base_height(400000); // phase3 first block (oracle boundary)
auto ws = fx.make();
auto tmpl = ws->get_current_work_template();
ASSERT_TRUE(tmpl.is_object());
ASSERT_TRUE(tmpl.contains("height"));
ASSERT_TRUE(tmpl.contains("coinbasevalue"));
// next_h = next_block_height() = base_height (empty chain) = 400000.
EXPECT_EQ(tmpl["height"].get<uint32_t>(), 400000u);
// Zero embedded fees, no external GBT -> oracle subsidy at the boundary.
EXPECT_EQ(tmpl["coinbasevalue"].get<uint64_t>(), 2434410000ULL);
}
// Stage 4c GBT scaffold: alongside height + coinbasevalue, the work template
// now surfaces the GBT fields the embedded path can derive truthfully without
// a TemplateBuilder port -- version (Scrypt algo lane), curtime, mintime, and
// an (empty) transactions[]. previousblockhash + bits intentionally stay absent
// until HeaderSample carries the tip hash / next-target compact (later slices).
TEST(DgbWorkSource, WorkTemplateEmitsGbtScaffoldFields)
{
Fixture fx;
fx.chain.set_base_height(400000);
auto ws = fx.make();
auto tmpl = ws->get_current_work_template();
ASSERT_TRUE(tmpl.is_object());
// version pins the DGB Scrypt lane: BIP9 base | algo nibble 0x0000.
ASSERT_TRUE(tmpl.contains("version"));
EXPECT_EQ(tmpl["version"].get<uint32_t>(), 0x20000000u);
// Empty chain -> median_time_past() == INT64_MIN -> mintime emitted as 0
// (unconstrained), and curtime is a real wall-clock stamp (>= 0).
ASSERT_TRUE(tmpl.contains("mintime"));
EXPECT_EQ(tmpl["mintime"].get<int64_t>(), 0);
ASSERT_TRUE(tmpl.contains("curtime"));
EXPECT_GE(tmpl["curtime"].get<int64_t>(), 0);
// No embedded tx selection yet -> transactions[] present but empty (no
// fabricated entries; consistent with the total_fees=0 coinbasevalue).
ASSERT_TRUE(tmpl.contains("transactions"));
EXPECT_TRUE(tmpl["transactions"].is_array());
EXPECT_TRUE(tmpl["transactions"].empty());
// The two hash/difficulty fields are deliberately NOT emitted yet.
EXPECT_FALSE(tmpl.contains("previousblockhash"));
EXPECT_FALSE(tmpl.contains("bits"));
}
// ── Embedded coinbasevalue: first production caller of subsidy_func ──────────
// One pin on each side of every DGB reward-era boundary (p2pool-dgb-scrypt
// oracle vectors, test_dgb_subsidy.cpp).
namespace {
struct EraVec { uint32_t height; uint64_t subsidy; const char* era; };
constexpr EraVec kEraBoundaries[] = {
{67199, 8000000000ULL, "phase1-fixed last"},
{67200, 7960000000ULL, "phase2 -0.5%/wk first"},
{399999, 6746441103ULL, "phase2 last"},
{400000, 2434410000ULL, "phase3 -1%/wk first"},
{1429999, 2157824200ULL, "phase3 last"},
{1430000, 1078500000ULL, "phase4 monthly-decay first"},
};
} // namespace
// No external GBT (embedded path): coinbasevalue is derived THROUGH the work
// source's subsidy_func at every era boundary, zero fees -> oracle subsidy.
TEST(DgbWorkSource, CoinbaseValueDerivesViaSubsidyFuncWhenNoGbt)
{
Fixture fx;
auto ws = fx.make();
for (const auto& v : kEraBoundaries) {
EXPECT_EQ(ws->coinbase_value(v.height, /*fees=*/0, std::nullopt), v.subsidy)
<< "embedded coinbasevalue diverged from oracle subsidy at " << v.era;
}
}
// Fees compose additively on the embedded path: subsidy + total_fees.
TEST(DgbWorkSource, CoinbaseValueAddsFeesOnEmbeddedPath)
{
Fixture fx;
auto ws = fx.make();
constexpr uint64_t kFees = 1234567ULL;
for (const auto& v : kEraBoundaries) {
EXPECT_EQ(ws->coinbase_value(v.height, kFees, std::nullopt), v.subsidy + kFees)
<< "fee addition wrong at " << v.era;
}
}
// External-daemon fallback PERSISTS: a present GBT coinbasevalue is authoritative
// and returned verbatim through the work source, bypassing local derivation.
TEST(DgbWorkSource, CoinbaseValueHonorsGbtVerbatim)
{
Fixture fx;
auto ws = fx.make();
constexpr uint64_t kGbt = 99999999999ULL; // deliberately != subsidy+fees
EXPECT_EQ(ws->coinbase_value(/*height=*/400000, /*fees=*/500,
std::optional<uint64_t>{kGbt}),
kGbt);
}
// Production-binding guard. The era-boundary tests above trust kSubsidyFunc, a
// hand-written DUPLICATE of params.hpp p.subsidy_func: they pin the schedule
// math but are blind to a regression in the ACTUAL wiring. If make_coin_params()
// shipped subsidy_func unbound (the work source then logs subsidy_func=UNSET and
// the embedded coinbasevalue silently falls through to the GBT-only path) or
// bound it to the wrong function, every test above would still pass off its
// private copy. Pin the LIVE production binding directly.
TEST(DgbCoinParams, SubsidyFuncBoundToOracleScheduleInProduction)
{
const core::CoinParams p = dgb::make_coin_params(/*testnet=*/false);
ASSERT_TRUE(static_cast<bool>(p.subsidy_func))
<< "make_coin_params shipped subsidy_func UNSET -- embedded coinbasevalue "
"would silently degrade to the external-GBT-only path";
for (const auto& v : kEraBoundaries) {
EXPECT_EQ(p.subsidy_func(v.height), v.subsidy)
<< "production subsidy_func diverged from oracle subsidy at " << v.era;
EXPECT_EQ(p.subsidy_func(v.height), kSubsidyFunc(v.height))
<< "production binding diverged from the schedule under test at " << v.era;
}
}
// 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"));
}
// The dedicated prevhash getter and the assembled template draw the tip hash
// from ONE source (chain_.tip_hash() through u256_be_display_hex), so they can
// never diverge: with a real tip the getter returns the SAME BE-display-hex the
// template emits; with no tip BOTH are absent (getter empty string, template
// omits the field).
TEST(DgbWorkSource, GbtPrevhashGetterMatchesTemplateField)
{
Fixture fx;
// No tip yet -> getter empty, template omits previousblockhash.
{
auto ws = fx.make();
EXPECT_TRUE(ws->get_current_gbt_prevhash().empty());
EXPECT_FALSE(ws->get_current_work_template().contains("previousblockhash"));
}
// Seed a Scrypt header carrying a distinctive block id (mirrors the
// previousblockhash emit test) -> getter == template field, BE-display-hex.
fx.chain.set_base_height(400000);
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();
const std::string expected = std::string(48, '0') + "123456789abcdef0";
EXPECT_EQ(ws->get_current_gbt_prevhash(), expected);
EXPECT_EQ(ws->get_current_work_template()["previousblockhash"].get<std::string>(),
expected);
}
} // namespace