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.
3950using 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
43101DGBWorkSource::DGBWorkSource (c2pool::dgb::HeaderChain& chain,
@@ -277,35 +335,210 @@ core::stratum::CoinbaseResult DGBWorkSource::build_connection_coinbase(
277335
278336nlohmann::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+ }
309542double 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