Skip to content

Commit 3866a46

Browse files
committed
coins: mint the block-winning share onto the sharechain (dash/btc/bch/dgb)
Fixes #887. mining_submit classifies a solve tighten-first: block target, then share target. On dash, btc, bch and dgb the block arm dispatched the won block and RETURNED, so the share arm -- the only place the sharechain mint / create-share seam is invoked -- was never reached. Because block_target <= share_target, a solve that clears the block target clears the share target by definition: it is the highest-work share the node will ever produce, and it was discarded on every block won. That costs (a) our own PPLNS weight in the very window the block pays out from and (b) peers' view of our best work. LTC is already correct -- core/web_server.cpp:7464 runs the create-share hook before and independently of the block check. Not block loss: the block submits correctly on all four coins. This is share-weight loss. Each coin's mint invocation is lifted VERBATIM out of the share arm into a local helper and invoked from BOTH arms. ORDERING. LTC mints first; here the helper runs on the won-block arm strictly AFTER the block has been dispatched (on dash, also after the payee-guard verdict and the recent-blocks record). The reward invariant is "never refuse a block the daemon would ACCEPT", so nothing may be inserted ahead of the submit: the mint walks the tracker, takes locks, can decline, and can throw. Running it downstream reaches the same end state as LTC with none of that exposure. The helper also catches exceptions locally on every coin (dgb's ShareAccept previously had none), so a mint fault can never cost the block or turn a won block into a stratum reject. dash: the mint runs even after a payee-guard reject. The guard is a COIN-side verdict about a stale masternode payee; the sharechain has its own acceptance rule (the mint's X11-identity + pow<=own-committed-target gates) and the share arm has never consulted the guard. The existing reject branch already states the share still counts for the miner. Withholding would be a second, self-inflicted loss on top of the forfeited block. dgb: PRE-WIRING ONLY. Per #884 main_dgb.cpp never calls set_mint_share_fn, so dgb cannot mint any local share today -- not this one, and not an ordinary ShareAccept either. The won-block arm now REACHES the seam so dgb inherits the fix the moment #884 binds it. The dgb KAT is named and commented accordingly and asserts only seam reachability. NO CONSENSUS CHANGE. Share construction, target derivation and serialisation live entirely inside the untouched mint / create_local_share seams. This changes only WHETHER the seam is invoked, never what it builds. Tests folded into EXISTING allowlisted targets (never a new add_executable, which would silently report "Not Run"). Each coin pins: the block still dispatches; the share seam is reached; the seam observes the block as ALREADY dispatched (ordering witness); and a throwing seam costs neither the block nor the accepted reply.
1 parent 3548684 commit 3866a46

10 files changed

Lines changed: 818 additions & 167 deletions

File tree

src/impl/bch/stratum/work_source.cpp

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,52 @@ nlohmann::json BCHWorkSource::mining_submit(
690690

691691
auto pow_hex_short = pow_hash.GetHex().substr(0, 16);
692692

693+
// -- Sharechain write (#887) --
694+
// Dispatch to create_share_fn_ (v36 share add). SHARED by both accept
695+
// classes: block_target <= share_target, so a solve that meets the block
696+
// target meets the share target too -- it is the highest-work share this
697+
// node will ever produce, and the sharechain and the coin blockchain are
698+
// independent systems. Before #887 the won-block arm returned without ever
699+
// reaching this seam, forfeiting our own PPLNS weight in the very window
700+
// the block pays out from and denying peers our best work. LTC has always
701+
// written on both classes (web_server.cpp).
702+
//
703+
// ORDERING -- reward invariant: on the won-block arm this runs AFTER the
704+
// block has already been dispatched, never before. create_share_fn_ takes
705+
// the exclusive tracker lock, can decline, and can throw; the block submit
706+
// must never sit behind any of that.
707+
//
708+
// CONSENSUS: unchanged. Share construction, target derivation and
709+
// serialisation live entirely inside the (untouched) create_local_share
710+
// seam -- this changes only WHETHER it is invoked, not what it builds.
711+
auto write_solved_share = [&](bool won_block) {
712+
const char* tag = won_block ? "[BCH-STRATUM-BLOCK-SHARE]"
713+
: "[BCH-STRATUM-SHARE]";
714+
CreateShareFn create_fn;
715+
{ std::lock_guard<std::mutex> lk(callback_mutex_); create_fn = create_share_fn_; }
716+
717+
uint256 share_hash;
718+
if (create_fn) {
719+
auto payout_script = core::address_to_script(username);
720+
try { share_hash = create_fn(coinbase, header, *job, payout_script); }
721+
catch (const std::exception& e) {
722+
LOG_WARNING << tag << " create_share_fn threw: " << e.what()
723+
<< " -- share not added";
724+
}
725+
}
726+
727+
if (!share_hash.IsNull())
728+
LOG_INFO << tag << " ACCEPTED + ADDED user=" << username
729+
<< " share_hash=" << share_hash.GetHex().substr(0, 16)
730+
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
731+
else if (create_fn)
732+
LOG_INFO << tag << " accepted (deferred) user=" << username
733+
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
734+
else
735+
LOG_INFO << tag << " accepted (no-tracker) user=" << username
736+
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
737+
};
738+
693739
// -- Classify --
694740
if (!(pow_hash > block_target)) {
695741
// BLOCK FOUND.
@@ -731,6 +777,11 @@ nlohmann::json BCHWorkSource::mining_submit(
731777
<< height << " not broadcast -- lost subsidy!";
732778
}
733779

780+
// #887: the won block is a SHARE too. The dispatch above has already
781+
// happened, so nothing here can delay, gate or endanger the block
782+
// submit -- the sharechain write is strictly downstream of it.
783+
write_solved_share(/*won_block=*/true);
784+
734785
{
735786
std::lock_guard<std::mutex> lk(workers_mutex_);
736787
for (auto& [sid, w] : workers_) { (void)sid; if (w.username == username) { w.accepted++; break; } }
@@ -740,29 +791,7 @@ nlohmann::json BCHWorkSource::mining_submit(
740791

741792
if (!(pow_hash > share_target)) {
742793
// Share meets sharechain target -> create_share_fn_ (v36 share add).
743-
CreateShareFn create_fn;
744-
{ std::lock_guard<std::mutex> lk(callback_mutex_); create_fn = create_share_fn_; }
745-
746-
uint256 share_hash;
747-
if (create_fn) {
748-
auto payout_script = core::address_to_script(username);
749-
try { share_hash = create_fn(coinbase, header, *job, payout_script); }
750-
catch (const std::exception& e) {
751-
LOG_WARNING << "[BCH-STRATUM-SHARE] create_share_fn threw: " << e.what()
752-
<< " -- share not added";
753-
}
754-
}
755-
756-
if (!share_hash.IsNull())
757-
LOG_INFO << "[BCH-STRATUM-SHARE] ACCEPTED + ADDED user=" << username
758-
<< " share_hash=" << share_hash.GetHex().substr(0, 16)
759-
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
760-
else if (create_fn)
761-
LOG_INFO << "[BCH-STRATUM-SHARE] accepted (deferred) user=" << username
762-
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
763-
else
764-
LOG_INFO << "[BCH-STRATUM-SHARE] accepted (no-tracker) user=" << username
765-
<< " pow_hash=" << pow_hex_short << " job=" << job_id;
794+
write_solved_share(/*won_block=*/false);
766795

767796
{
768797
std::lock_guard<std::mutex> lk(workers_mutex_);

src/impl/bch/test/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,22 @@ if(BUILD_TESTING)
212212
# Needs coin/transaction.cpp for the MutableTransaction ctors, same as
213213
# block_connector_test; link set and header-only-over-coin/*.hpp posture are
214214
# otherwise identical, so per-coin isolation stays clean.
215+
#
216+
# #887 fold-in: the same file now also drives BCHWorkSource::mining_submit
217+
# to pin that a WON BLOCK is written to the sharechain as well (block
218+
# dispatch first, share write strictly after). That needs bch_stratum
219+
# (work_source.cpp) and its bch_coin dependency. FOLDED into this EXISTING
220+
# allowlisted target rather than a new add_executable -- a new one would
221+
# silently report "Not Run" (see merged PR #868). Still bch-only, so the
222+
# per-coin isolation invariant holds.
215223
add_executable(bch_g2_block_assembly_roundtrip_test
216224
g2_block_assembly_roundtrip_test.cpp
217225
${CMAKE_SOURCE_DIR}/src/impl/bch/coin/transaction.cpp)
218226
target_include_directories(bch_g2_block_assembly_roundtrip_test PRIVATE ${CMAKE_SOURCE_DIR}/src)
219227
target_link_libraries(bch_g2_block_assembly_roundtrip_test PRIVATE
220228
core pool sharechain
221229
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage c2pool_difficulty
230+
bch_stratum bch_coin
222231
btclibs)
223232
add_test(NAME bch_g2_block_assembly_roundtrip_test COMMAND bch_g2_block_assembly_roundtrip_test)
224233
endif()

src/impl/bch/test/g2_block_assembly_roundtrip_test.cpp

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@
4949
#include <string>
5050
#include <vector>
5151

52+
#include <memory>
53+
#include <stdexcept>
54+
5255
#include "../coin/block.hpp"
5356
#include "../coin/block_assembly.hpp"
57+
#include "../coin/header_chain.hpp"
5458
#include "../coin/mempool.hpp"
5559
#include "../coin/merkle.hpp"
5660
#include "../coin/reconstruct_won_block.hpp"
5761
#include "../coin/transaction.hpp"
5862
#include "../stratum/coinbase_slot_guard.hpp"
63+
#include "../stratum/work_source.hpp"
64+
65+
#include <core/stratum_types.hpp>
5966

6067
namespace {
6168

@@ -349,6 +356,138 @@ void test_truncation_and_trailing_bytes()
349356
CHECK(std::string(hv.reason).find("no transactions") != std::string::npos);
350357
}
351358

359+
// ---------------------------------------------------------------------------
360+
// #887 -- the block-winning share must ALSO be written to the sharechain.
361+
//
362+
// BCHWorkSource::mining_submit classifies tighten-first: block target, then
363+
// share target. block_target <= share_target, so a solve that clears the block
364+
// target clears the share target BY DEFINITION -- it is the highest-work share
365+
// this node will ever produce. Before #887 the won-block arm dispatched the
366+
// block and RETURNED, so create_share_fn_ was never reached and that share was
367+
// discarded: our own PPLNS weight in the very window the block pays out from,
368+
// and the strongest share peers would ever have seen from us, forfeited on
369+
// every block won. LTC has always written on both classes (core/web_server.cpp).
370+
//
371+
// The sharechain and the coin blockchain are independent systems; the solve
372+
// belongs in both.
373+
//
374+
// Determinism: SHA256d of a fixed header is not steerable, so the outcome CLASS
375+
// is pinned by the TARGETS, not the hash (the dgb_work_source_test idiom).
376+
// block_nbits 0x2100ffff expands to 0xffff << 240 (~2^256): every practical
377+
// digest clears it -> WonBlock. 0x03000001 expands to 1: none clears it.
378+
// ---------------------------------------------------------------------------
379+
380+
struct WorkSourceRig {
381+
bch::coin::BCHChainParams params = bch::coin::BCHChainParams::mainnet();
382+
bch::coin::HeaderChain chain{params};
383+
bch::coin::Mempool mempool;
384+
bool submit_called = false;
385+
386+
std::unique_ptr<bch::stratum::BCHWorkSource> make()
387+
{
388+
auto fn = [this](const std::vector<unsigned char>&, uint32_t) -> bool {
389+
submit_called = true;
390+
return true;
391+
};
392+
return std::make_unique<bch::stratum::BCHWorkSource>(
393+
chain, mempool, /*is_testnet=*/false, fn);
394+
}
395+
};
396+
397+
core::stratum::JobSnapshot make_submit_job(uint32_t share_bits,
398+
const std::string& block_nbits)
399+
{
400+
core::stratum::JobSnapshot j;
401+
j.coinb1 = "01000000"; // minimal well-formed coinbase head
402+
j.coinb2 = "00000000"; // minimal coinbase tail
403+
j.gbt_prevhash = std::string(64, '0'); // 32-byte prevhash, BE display hex
404+
j.nbits = "1e0fffff"; // header (share) bits
405+
j.version = 0x20000000u;
406+
j.share_bits = share_bits;
407+
j.block_nbits = block_nbits;
408+
j.subsidy = 312500000ULL;
409+
return j;
410+
}
411+
412+
void test_won_block_also_writes_share()
413+
{
414+
WorkSourceRig rig;
415+
auto ws = rig.make();
416+
417+
bool share_written = false;
418+
bool saw_block_already_submitted = false;
419+
std::size_t seen_header_size = 0;
420+
ws->set_create_share_fn(
421+
[&](const std::vector<unsigned char>&,
422+
const std::vector<uint8_t>& header_80b,
423+
const core::stratum::JobSnapshot&,
424+
const std::vector<unsigned char>&) -> uint256
425+
{
426+
share_written = true;
427+
seen_header_size = header_80b.size();
428+
// Reward-invariant witness: the block MUST already be dispatched.
429+
saw_block_already_submitted = rig.submit_called;
430+
return uint256(uint64_t(0xb10c5));
431+
});
432+
433+
auto job = make_submit_job(/*share_bits=*/0x2100ffffu, /*block_nbits=*/"2100ffff");
434+
auto result = ws->mining_submit(
435+
"bchtest.worker1", "job-won-share", "00000000", "00000000",
436+
"60000000", "00000000", "rid", /*merged_addresses=*/{}, &job);
437+
438+
CHECK(result.is_boolean() && result.get<bool>());
439+
CHECK(rig.submit_called); // block dispatched, unchanged
440+
CHECK(share_written); // ...AND the share is written
441+
CHECK(saw_block_already_submitted); // block FIRST, always
442+
CHECK(seen_header_size == 80u);
443+
}
444+
445+
void test_won_block_survives_throwing_share_write()
446+
{
447+
WorkSourceRig rig;
448+
auto ws = rig.make();
449+
ws->set_create_share_fn(
450+
[](const std::vector<unsigned char>&,
451+
const std::vector<uint8_t>&,
452+
const core::stratum::JobSnapshot&,
453+
const std::vector<unsigned char>&) -> uint256 {
454+
throw std::runtime_error("share write blew up");
455+
});
456+
457+
auto job = make_submit_job(/*share_bits=*/0x2100ffffu, /*block_nbits=*/"2100ffff");
458+
auto result = ws->mining_submit(
459+
"bchtest.worker1", "job-won-throw", "00000000", "00000000",
460+
"60000000", "00000000", "rid", /*merged_addresses=*/{}, &job);
461+
462+
CHECK(rig.submit_called); // block reached the sink
463+
CHECK(result.is_boolean() && result.get<bool>()); // throw did not escape
464+
}
465+
466+
void test_plain_share_still_writes_and_does_not_broadcast()
467+
{
468+
WorkSourceRig rig;
469+
auto ws = rig.make();
470+
bool share_written = false;
471+
ws->set_create_share_fn(
472+
[&](const std::vector<unsigned char>&,
473+
const std::vector<uint8_t>&,
474+
const core::stratum::JobSnapshot&,
475+
const std::vector<unsigned char>&) -> uint256 {
476+
share_written = true;
477+
return uint256(uint64_t(0x5157));
478+
});
479+
480+
// block target 1 -> no digest is a block; share target maximal -> accepted.
481+
auto job = make_submit_job(/*share_bits=*/0x2100ffffu, /*block_nbits=*/"03000001");
482+
auto result = ws->mining_submit(
483+
"bchtest.worker1", "job-share", "00000000", "00000000",
484+
"60000000", "00000000", "rid", /*merged_addresses=*/{}, &job);
485+
486+
CHECK(result.is_boolean() && result.get<bool>());
487+
CHECK(share_written);
488+
CHECK(!rig.submit_called);
489+
}
490+
352491
} // namespace
353492

354493
int main()
@@ -358,6 +497,9 @@ int main()
358497
test_degraded_reference_hash_absent();
359498
test_job_builder_commitment_slot_invariant();
360499
test_truncation_and_trailing_bytes();
500+
test_won_block_also_writes_share();
501+
test_won_block_survives_throwing_share_write();
502+
test_plain_share_still_writes_and_does_not_broadcast();
361503

362504
if (failures) {
363505
std::cerr << failures << " CHECK(s) failed\n";

0 commit comments

Comments
 (0)