Skip to content

Commit d6fcd93

Browse files
authored
dgb: wire mining_submit live invocation (reconstruct->scrypt_pow->classify->dispatch) [#82 leg-2 / Stage 4d] (#297)
Replace the Stage-4a mining_submit stub with the live hot path. Reconstruct the 80-byte DGB block header from the JobSnapshot + miner inputs (coinbase = coinb1||en1||en2||coinb2; merkle ascent from the coinbase txid; header = version|prev|merkle|ntime|nbits|nonce, all little-endian / prevhash reversed to internal order), run the DGB-Scrypt PoW digest through the #286 scrypt_pow SSOT, expand both compact targets via compact_to_target, and classify via the Stage-4d submit_classify SSOT (tighten-first): WonBlock -> serialize the full block (BIP144 coinbase witness when segwit active) and hand it to the dual-path broadcaster submit_block_fn (P2P relay + submitblock RPC fallback, #82); scream-never-drop on a won block that reaches no sink. ShareAccept -> forward the found-share fields to try_mint_share (the #295 producer seam -> #294 mint bind); degrades to accepted-without- credit when no mint fn is wired (no silent drop). Reject -> stratum low-difficulty error. work_source.cpp / work_source_test.cpp only; per-coin isolation held. The header layout is byte-for-byte Bitcoin (only the PoW digest differs, Scrypt); no share format, coinbase commitment, or PPLNS math touched -> p2pool-merged-v36 surface NONE. +3 KATs pin the three-way ladder dispatch by job targets (23/23). Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent fdffce5 commit d6fcd93

2 files changed

Lines changed: 352 additions & 25 deletions

File tree

src/impl/dgb/stratum/work_source.cpp

Lines changed: 258 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,21 @@
2121
#include <impl/dgb/coin/dgb_block_algo.hpp>
2222
#include <impl/dgb/coin/template_builder.hpp>
2323
#include <impl/dgb/coin/hash_format.hpp>
24+
#include <impl/dgb/coin/scrypt_pow.hpp> // scrypt_pow_hash (DGB-Scrypt PoW SSOT)
25+
#include <impl/dgb/coin/submit_classify.hpp> // classify_submission (Stage-4d decision SSOT)
26+
#include <impl/dgb/coin/header_sample_build.hpp>// compact_to_target (compact bits -> u256)
2427

2528
#include <core/log.hpp>
29+
#include <core/hash.hpp> // Hash (sha256d) for coinbase txid + merkle pair
30+
#include <core/address_utils.hpp> // address_to_script (share payout from username)
2631

32+
#include <btclibs/util/strencodings.h> // ParseHex
33+
34+
#include <array>
35+
#include <cstdio>
36+
#include <cstring>
2737
#include <ctime>
38+
#include <span>
2839
#include <string>
2940
#include <limits>
3041

@@ -38,6 +49,53 @@ namespace {
3849
// cannot emit a divergent previousblockhash encoding.
3950
using dgb::coin::u256_be_display_hex;
4051

52+
// -- Byte-stream helpers for the Stage-4d header/block reconstruction --------
53+
// Little-endian Bitcoin wire encodings, identical to btc::stratum's own
54+
// mining_submit assembly (the DGB header layout is byte-for-byte Bitcoin's;
55+
// only the PoW digest differs -- Scrypt, not SHA256d).
56+
inline void push_u32_le(std::vector<uint8_t>& v, uint32_t x) {
57+
v.push_back(static_cast<uint8_t>( x & 0xff));
58+
v.push_back(static_cast<uint8_t>((x >> 8) & 0xff));
59+
v.push_back(static_cast<uint8_t>((x >> 16) & 0xff));
60+
v.push_back(static_cast<uint8_t>((x >> 24) & 0xff));
61+
}
62+
63+
inline void push_u64_le(std::vector<uint8_t>& v, uint64_t x) {
64+
for (int i = 0; i < 8; ++i)
65+
v.push_back(static_cast<uint8_t>((x >> (i * 8)) & 0xff));
66+
}
67+
68+
inline void push_varint(std::vector<uint8_t>& v, uint64_t n) {
69+
if (n < 0xfd) {
70+
v.push_back(static_cast<uint8_t>(n));
71+
} else if (n <= 0xffff) {
72+
v.push_back(0xfd);
73+
v.push_back(static_cast<uint8_t>(n & 0xff));
74+
v.push_back(static_cast<uint8_t>((n >> 8) & 0xff));
75+
} else if (n <= 0xffffffff) {
76+
v.push_back(0xfe);
77+
push_u32_le(v, static_cast<uint32_t>(n));
78+
} else {
79+
v.push_back(0xff);
80+
push_u64_le(v, n);
81+
}
82+
}
83+
84+
// Stratum sends ntime/nonce/version as big-endian hex; sscanf(%x) decodes them.
85+
inline uint32_t parse_be_hex_u32(const std::string& s) {
86+
uint32_t v = 0;
87+
std::sscanf(s.c_str(), "%x", &v);
88+
return v;
89+
}
90+
91+
// One merkle ascent step: sha256d(left||right) over the two LE-internal 32-byte
92+
// node hashes (the dgb-local equivalent of btc::coin::merkle_hash_pair -- kept
93+
// in-tree per the per-coin isolation invariant rather than reaching into btc).
94+
inline uint256 merkle_pair(const uint256& left, const uint256& right) {
95+
return Hash(std::span<const uint8_t>(left.data(), 32),
96+
std::span<const uint8_t>(right.data(), 32));
97+
}
98+
4199
} // namespace
42100

43101
DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain,
@@ -277,35 +335,210 @@ core::stratum::CoinbaseResult DGBWorkSource::build_connection_coinbase(
277335

278336
nlohmann::json DGBWorkSource::mining_submit(
279337
const std::string& username, const std::string& job_id,
280-
const std::string& /*extranonce1*/, const std::string& /*extranonce2*/,
281-
const std::string& /*ntime*/, const std::string& /*nonce*/,
338+
const std::string& extranonce1, const std::string& extranonce2,
339+
const std::string& ntime, const std::string& nonce,
282340
const std::string& /*request_id*/,
283341
const std::map<uint32_t, std::vector<unsigned char>>& /*merged_addresses*/,
284-
const core::stratum::JobSnapshot* /*job*/)
342+
const core::stratum::JobSnapshot* job)
285343
{
286-
// Stage 4d will:
287-
// 1. Reconstruct the 80-byte block header from JobSnapshot + miner inputs
288-
// 2. Scrypt(header) → pow_hash (scrypt_1024_1_1_256, the DGB-Scrypt algo)
289-
// 3. Decode share target from job->share_bits (compact)
290-
// 4. Decode block target from job->block_nbits (compact)
291-
// 5. Classify:
292-
// pow_hash <= block target → submit_block_fn_(full_block, height)
293-
// pow_hash <= share target → record share in tracker (sharechain accept)
294-
// otherwise → reject as low-difficulty
295-
// 6. Update worker stats accordingly
296-
//
297-
// For now: log + reject everything as low-difficulty. Miners will see
298-
// stratum errors but the binary won't crash.
299-
LOG_WARNING << "[DGB-STRATUM] mining_submit not implemented (stage 4d): "
300-
<< "user=" << username << " job=" << job_id
301-
<< " — submission rejected as low-difficulty";
302-
303-
return nlohmann::json::array({
304-
false,
305-
nlohmann::json::array({23, "Low difficulty share (stratum stub: stage 4d not implemented)", nullptr})
306-
});
307-
}
344+
// Stage 4d -- the hot path. Reconstruct the 80-byte block header from the
345+
// JobSnapshot + miner inputs, run the DGB-Scrypt PoW digest, and place the
346+
// result in EXACTLY ONE of three classes via the decision SSOT
347+
// (coin/submit_classify.hpp): WonBlock -> dual-path broadcaster (#82),
348+
// ShareAccept -> sharechain mint (try_mint_share -> #294), Reject -> stratum
349+
// low-difficulty error. The header assembly mirrors btc::stratum byte-for-
350+
// byte (the DGB header layout IS Bitcoin's); ONLY the PoW digest differs
351+
// (scrypt_1024_1_1_256, the sole algo c2pool-dgb validates in V36).
352+
353+
// Stratum JSON-RPC error payload (false + [code, message, null]).
354+
auto reject = [](int code, const char* msg) {
355+
return nlohmann::json::array({
356+
false, nlohmann::json::array({code, msg, nullptr})
357+
});
358+
};
359+
360+
if (!job) {
361+
LOG_WARNING << "[DGB-STRATUM] submit reject (no JobSnapshot): user=" << username
362+
<< " job=" << job_id;
363+
return reject(21, "Job not found");
364+
}
365+
366+
// 1. coinbase = coinb1 || extranonce1 || extranonce2 || coinb2
367+
auto coinb1_bytes = ParseHex(job->coinb1);
368+
auto en1_bytes = ParseHex(extranonce1);
369+
auto en2_bytes = ParseHex(extranonce2);
370+
auto coinb2_bytes = ParseHex(job->coinb2);
371+
372+
std::vector<uint8_t> coinbase;
373+
coinbase.reserve(coinb1_bytes.size() + en1_bytes.size()
374+
+ en2_bytes.size() + coinb2_bytes.size());
375+
coinbase.insert(coinbase.end(), coinb1_bytes.begin(), coinb1_bytes.end());
376+
coinbase.insert(coinbase.end(), en1_bytes.begin(), en1_bytes.end());
377+
coinbase.insert(coinbase.end(), en2_bytes.begin(), en2_bytes.end());
378+
coinbase.insert(coinbase.end(), coinb2_bytes.begin(), coinb2_bytes.end());
379+
380+
// 2. coinbase_txid = sha256d(coinbase) (non-witness txid for the merkle root)
381+
uint256 coinbase_txid = Hash(
382+
std::span<const uint8_t>(coinbase.data(), coinbase.size()));
383+
384+
// 3. merkle_root = ascend the frozen stratum merkle branches from the txid.
385+
// Branches are LE-internal bytes (ParseHex+memcpy, NOT SetHex which
386+
// reverses). Keep the parsed branches to hand to the share-mint path.
387+
std::vector<uint256> branch_hashes;
388+
branch_hashes.reserve(job->merkle_branches.size());
389+
uint256 merkle_root = coinbase_txid;
390+
for (const auto& branch_hex : job->merkle_branches) {
391+
uint256 b;
392+
auto bb = ParseHex(branch_hex);
393+
if (bb.size() == 32) std::memcpy(b.begin(), bb.data(), 32);
394+
branch_hashes.push_back(b);
395+
merkle_root = merkle_pair(merkle_root, b);
396+
}
397+
398+
// 4. header = version(LE) || prev(LE) || merkle_root || ntime(LE)
399+
// || nbits(LE) || nonce(LE) -- prevhash arrives BE-display, so
400+
// reverse to internal byte order.
401+
auto prevhash_be = ParseHex(job->gbt_prevhash);
402+
std::vector<uint8_t> prevhash_le(prevhash_be.rbegin(), prevhash_be.rend());
403+
404+
std::vector<uint8_t> header;
405+
header.reserve(80);
406+
push_u32_le(header, job->version);
407+
header.insert(header.end(), prevhash_le.begin(), prevhash_le.end());
408+
header.insert(header.end(), merkle_root.data(), merkle_root.data() + 32);
409+
push_u32_le(header, parse_be_hex_u32(ntime));
410+
push_u32_le(header, parse_be_hex_u32(job->nbits)); // header carries share bits
411+
push_u32_le(header, parse_be_hex_u32(nonce));
412+
413+
if (header.size() != 80) {
414+
LOG_WARNING << "[DGB-STRATUM] submit reject (reconstructed header "
415+
<< header.size() << "B != 80): user=" << username
416+
<< " job=" << job_id;
417+
return reject(20, "Malformed header reconstruction");
418+
}
308419

420+
// 5. DGB-Scrypt PoW digest (scrypt_1024_1_1_256, the ONLY algo V36 validates).
421+
dgb::coin::u256 pow_hash = dgb::coin::scrypt_pow_hash(header.data());
422+
423+
// Expand both compact targets to u256 (compact_to_target SSOT). Fall back to
424+
// a permissive diff-1 share target for jobs frozen before share_bits was set
425+
// so a missing share target never silently rejects every share.
426+
dgb::coin::u256 share_target = c2pool::dgb::compact_to_target(
427+
job->share_bits != 0 ? job->share_bits : 0x1d00ffffu);
428+
dgb::coin::u256 block_target = c2pool::dgb::compact_to_target(parse_be_hex_u32(
429+
job->block_nbits.empty() ? job->nbits : job->block_nbits));
430+
431+
// 6. Classify via the Stage-4d SSOT (tighten-first: block target before share).
432+
const dgb::coin::SubmitClass klass =
433+
dgb::coin::classify_submission(pow_hash, block_target, share_target);
434+
435+
auto bump_accepted = [&] {
436+
std::lock_guard<std::mutex> lk(workers_mutex_);
437+
for (auto& kv : workers_) {
438+
if (kv.second.username == username) { kv.second.accepted++; break; }
439+
}
440+
};
441+
442+
switch (klass) {
443+
case dgb::coin::SubmitClass::WonBlock: {
444+
// pow_hash <= block_target -> a full network block. NEVER drop it.
445+
const uint32_t height = chain_.next_block_height();
446+
LOG_WARNING << "[DGB-STRATUM-BLOCK] *** BLOCK FOUND *** user=" << username
447+
<< " height~=" << height << " job=" << job_id;
448+
449+
// Serialize the block: header || tx_count || coinbase[+witness] || txs.
450+
// A segwit-active coinbase is reserialized in BIP144 form with the
451+
// 32-byte witness reserved value (digibyted validates the aa21a9ed
452+
// commitment by hashing witness_root||reserved; a missing witness ->
453+
// bad-witness-merkle-match -> block rejected).
454+
std::vector<uint8_t> coinbase_serialized = coinbase;
455+
if (job->segwit_active) {
456+
const std::array<uint8_t, 2> marker_flag = {0x00, 0x01};
457+
coinbase_serialized.insert(coinbase_serialized.begin() + 4,
458+
marker_flag.begin(), marker_flag.end());
459+
std::array<uint8_t, 34> witness_bytes{};
460+
witness_bytes[0] = 0x01; // stack_count = 1
461+
witness_bytes[1] = 0x20; // item_len = 32 (reserved value, all zero)
462+
coinbase_serialized.insert(coinbase_serialized.end() - 4,
463+
witness_bytes.begin(), witness_bytes.end());
464+
}
465+
466+
static const std::vector<std::string> kEmptyTxData;
467+
const std::vector<std::string>& txs =
468+
job->tx_data ? *job->tx_data : kEmptyTxData;
469+
470+
std::vector<uint8_t> block_bytes;
471+
block_bytes.reserve(80 + 9 + coinbase_serialized.size() + txs.size() * 256);
472+
block_bytes.insert(block_bytes.end(), header.begin(), header.end());
473+
push_varint(block_bytes, 1 + txs.size()); // total tx count (coinbase + others)
474+
block_bytes.insert(block_bytes.end(),
475+
coinbase_serialized.begin(), coinbase_serialized.end());
476+
for (const auto& tx_hex : txs) {
477+
auto tx_bytes = ParseHex(tx_hex);
478+
block_bytes.insert(block_bytes.end(), tx_bytes.begin(), tx_bytes.end());
479+
}
480+
481+
// Dual-path broadcaster (#82): submit_block_fn_ relays via P2P (primary)
482+
// and falls back to the submitblock RPC; true iff it reached >=1 sink. A
483+
// won block reaching NEITHER is a lost subsidy -- scream, never drop.
484+
bool reached_network = false;
485+
if (submit_block_fn_) {
486+
try {
487+
reached_network = submit_block_fn_(block_bytes, height);
488+
} catch (const std::exception& e) {
489+
LOG_ERROR << "[DGB-STRATUM-BLOCK] submit_block_fn threw: " << e.what();
490+
}
491+
if (!reached_network) {
492+
LOG_ERROR << "[DGB-STRATUM-BLOCK] WON BLOCK height=" << height
493+
<< " reached NEITHER P2P relay NOR submitblock RPC -- "
494+
<< "lost subsidy!";
495+
}
496+
} else {
497+
LOG_ERROR << "[DGB-STRATUM-BLOCK] no submit_block_fn wired -- WON BLOCK"
498+
<< " height=" << height << " not broadcast -- lost subsidy!";
499+
}
500+
501+
bump_accepted();
502+
return nlohmann::json(true);
503+
}
504+
505+
case dgb::coin::SubmitClass::ShareAccept: {
506+
// share_target >= pow_hash > block_target -> meets sharechain difficulty
507+
// but not the full block. Hand the found-share fields to the run-loop
508+
// mint dispatch (try_mint_share -> mint_local_share_with_ratchet, #294).
509+
MintShareInputs in;
510+
in.header_bytes = header;
511+
in.coinbase_bytes = coinbase;
512+
in.subsidy = job->subsidy;
513+
in.prev_share = job->prev_share_hash;
514+
in.merkle_branches = branch_hashes;
515+
in.payout_script = core::address_to_script(username);
516+
in.segwit_active = job->segwit_active;
517+
518+
uint256 share_hash = try_mint_share(in);
519+
520+
if (!share_hash.IsNull()) {
521+
LOG_INFO << "[DGB-STRATUM-SHARE] ACCEPTED + MINTED user=" << username
522+
<< " share_hash=" << share_hash.GetHex().substr(0, 16)
523+
<< " job=" << job_id;
524+
} else {
525+
// PoW was valid (cleared the share target) so the miner still gets a
526+
// success reply; the share just earned no sharechain credit (no mint
527+
// fn wired, or the mint deferred/failed). try_mint_share logs the
528+
// reason -- never a silent drop.
529+
LOG_INFO << "[DGB-STRATUM-SHARE] accepted (no sharechain credit) user="
530+
<< username << " job=" << job_id;
531+
}
532+
533+
bump_accepted();
534+
return nlohmann::json(true);
535+
}
536+
537+
case dgb::coin::SubmitClass::Reject:
538+
default:
539+
return reject(23, "Low difficulty share");
540+
}
541+
}
309542
double DGBWorkSource::compute_share_difficulty(
310543
const std::string& /*coinb1*/, const std::string& /*coinb2*/,
311544
const std::string& /*extranonce1*/, const std::string& /*extranonce2*/,

0 commit comments

Comments
 (0)