From 1f4f8002f95b6b14de81396e6208de933cf2361f Mon Sep 17 00:00:00 2001 From: frstrtr Date: Mon, 22 Jun 2026 11:08:47 +0000 Subject: [PATCH] dgb: per-connection Stratum coinbase assembler + coinb1/coinb2 split SSOT Add coin/connection_coinbase.hpp: build_connection_coinbase_parts() builds the prospective next-share gentx for one Stratum connection via the assemble_gentx_coinbase SSOT, then splits it into the Stratum coinb1/coinb2 pair around the OP_RETURN 8-byte extranonce slot so that coinb1 || extranonce1(4) || extranonce2(4) || coinb2 reconstructs the exact gentx bytes the submit path validates. KAT (4) pins the assembled bytes byte-identical to the canonical frstrtr/p2pool-dgb-scrypt oracle wire (same NOSEG vector as the gentx assembler KAT) and the split invariant. Pure / no tracker dependency. Fenced to the DGB tree; runtime byte-unchanged (new file + test only). --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/connection_coinbase.hpp | 106 ++++++++++++++++++ src/impl/dgb/test/CMakeLists.txt | 16 +++ .../dgb/test/connection_coinbase_test.cpp | 104 +++++++++++++++++ 4 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/connection_coinbase.hpp create mode 100644 src/impl/dgb/test/connection_coinbase_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index daca62dae..c3ee24fac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,7 +81,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ - dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ + dgb_gentx_coinbase_test dgb_connection_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test v37_test \ @@ -213,7 +213,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ - dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ + dgb_gentx_coinbase_test dgb_connection_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ diff --git a/src/impl/dgb/coin/connection_coinbase.hpp b/src/impl/dgb/coin/connection_coinbase.hpp new file mode 100644 index 000000000..b6071bad1 --- /dev/null +++ b/src/impl/dgb/coin/connection_coinbase.hpp @@ -0,0 +1,106 @@ +#pragma once +// ============================================================================ +// connection_coinbase.hpp — per-connection Stratum coinbase assembler + split. +// +// Builds the gentx (via the gentx_coinbase.hpp SSOT) for the prospective NEXT +// share at a single Stratum connection, then splits it into the Stratum +// coinb1/coinb2 pair around the 8-byte extranonce slot embedded in the +// OP_RETURN ref commitment. The invariant the submit path relies on: +// +// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes +// +// c2pool places the per-miner extranonce in the OP_RETURN's last_txout_nonce +// slot (NOT the scriptSig), so the split point is purely positional: the +// 8-byte nonce occupies bytes [N-12, N-4) and the 4-byte locktime is [N-4, N). +// This mirrors frstrtr/p2pool-dgb-scrypt work.py get_work() coinbase framing. +// +// Pure: takes an already-resolved scriptSig, the PPLNS payout outputs, the +// donation pair, and the (ref_hash, last_txout_nonce) computed by the caller's +// ShareTracker seam. No tracker / chain dependency, so it is directly KAT-able +// against the canonical oracle wire (see test/connection_coinbase_test.cpp). +// ============================================================================ + +#include "gentx_coinbase.hpp" + +#include +#include // HexStr +#include // Span + +#include +#include +#include +#include +#include + +namespace dgb::coin +{ + +// Caller-resolved inputs for one connection's coinbase. payout_outputs are the +// PPLNS (scriptPubKey, value) pairs already in final consensus order (sorted +// asc by amount, asc by script — see share_check.hpp generate_share_transaction +// step 3). segwit_commitment_script present iff this share carries segwit data. +struct ConnCoinbaseInputs +{ + std::vector coinbase_script; // scriptSig (BIP34 height + pool tag) + std::optional> segwit_commitment_script; + std::vector, uint64_t>> payout_outputs; + uint64_t donation_amount{0}; + std::vector donation_script; + uint256 ref_hash; // p2pool ref_hash (32B) + uint64_t last_txout_nonce{0}; // OP_RETURN nonce (extranonce slot) +}; + +struct ConnCoinbaseParts +{ + std::string coinb1; // hex: gentx bytes up to (not incl.) the 8B nonce slot + std::string coinb2; // hex: locktime tail (the 4 bytes after the nonce slot) + GentxCoinbase gentx; // full non-witness bytes + txid (nonce slot filled in) +}; + +// OP_RETURN ref commitment script: 0x6a 0x28 || ref_hash(32 LE) || nonce(8 LE). +// Byte-identical to share_check.hpp's commitment (the verification SSOT). +inline std::vector build_ref_op_return(const uint256& ref_hash, + uint64_t last_txout_nonce) +{ + std::vector s; + s.reserve(2 + 32 + 8); + s.push_back(0x6a); // OP_RETURN + s.push_back(0x28); // PUSH 40 (32 ref_hash + 8 nonce) + auto rb = ref_hash.GetChars(); // 32 LE bytes + s.insert(s.end(), rb.begin(), rb.end()); + for (int i = 0; i < 8; ++i) + s.push_back(static_cast((last_txout_nonce >> (8 * i)) & 0xff)); + return s; +} + +// Assemble the per-connection coinbase and split it for Stratum. +inline ConnCoinbaseParts build_connection_coinbase_parts(const ConnCoinbaseInputs& in) +{ + auto op_return = build_ref_op_return(in.ref_hash, in.last_txout_nonce); + + GentxCoinbase g = assemble_gentx_coinbase( + in.coinbase_script, + in.segwit_commitment_script, + in.payout_outputs, + in.donation_amount, + in.donation_script, + op_return); + + // Positional split: OP_RETURN is the final vout, so its 8-byte + // last_txout_nonce is at [N-12, N-4) and the locktime at [N-4, N). + // coinb1 = bytes[0, N-12) (up to & incl. ref_hash) + // coinb2 = bytes[N-4, N) (locktime) + // The dropped 8 bytes are filled at submit by extranonce1 || extranonce2. + const auto& b = g.bytes; + const size_t n = b.size(); + // n is always >= 12 (vin + >=1 vout + op_return(51) + locktime(4)); the + // op_return alone is 51 bytes, so this holds for every valid assembly. + + ConnCoinbaseParts out; + out.coinb1 = HexStr(Span(b.data(), n - 12)); + out.coinb2 = HexStr(Span(b.data() + (n - 4), 4)); + out.gentx = std::move(g); + return out; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index afb84584f..d3ca49720 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -145,6 +145,22 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_gentx_coinbase_test "" AUTO) + # --- Phase B: per-connection Stratum coinbase assembler + split KAT ----- + # Pins coin/connection_coinbase.hpp: build_connection_coinbase_parts builds + # the per-connection gentx via the assemble_gentx_coinbase SSOT and splits + # it into coinb1/coinb2 around the OP_RETURN 8-byte extranonce slot. Proves + # bytes==oracle (same NOSEG vector as dgb_gentx_coinbase_test) AND the + # split invariant coinb1||en1||en2||coinb2 == full gentx. Links the dgb + # OBJECT-lib SCC set like dgb_gentx_coinbase_test. MUST appear in BOTH this + # registration AND the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_connection_coinbase_test connection_coinbase_test.cpp) + target_link_libraries(dgb_connection_coinbase_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_connection_coinbase_test "" AUTO) + # --- #172 share-path gentx KAT (collapse consumption proof) ------------ # Proves generate_share_transaction (the verification SSOT call site #172 # rewired) EMITS assemble_gentx_coinbase framing for a real v36 share, via diff --git a/src/impl/dgb/test/connection_coinbase_test.cpp b/src/impl/dgb/test/connection_coinbase_test.cpp new file mode 100644 index 000000000..7c2146baf --- /dev/null +++ b/src/impl/dgb/test/connection_coinbase_test.cpp @@ -0,0 +1,104 @@ +// DGB Phase B — per-connection Stratum coinbase assembler + split KAT. +// +// Locks dgb::coin::build_connection_coinbase_parts() (coin/connection_coinbase.hpp) +// against the SAME ground-truth oracle vector used by the gentx assembler KAT +// (gentx_coinbase_test.cpp NOSEG_BYTES — derived from frstrtr/p2pool-dgb-scrypt +// util/pack.py + donation 4104ffd0...ac). A PASS proves: +// +// 1. the per-connection coinbase serializes byte-identical to the oracle wire +// (the assembler path used == the verification SSOT), AND +// 2. the Stratum split invariant holds: +// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes +// with the 8-byte extranonce slot landing exactly on the OP_RETURN +// last_txout_nonce (the submit path reconstructs the header off this). +// +// Pure / no tracker: the (ref_hash, last_txout_nonce) are fixed inputs, so the +// vector is the oracle serializer's output — not self-generated. + +#include +#include + +#include +#include +#include +#include + +namespace { + +std::vector unhex(const std::string& h) { + std::vector v; v.reserve(h.size() / 2); + auto nyb = [](char c) -> int { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; }; + for (size_t i = 0; i + 1 < h.size(); i += 2) + v.push_back(static_cast((nyb(h[i]) << 4) | nyb(h[i + 1]))); + return v; +} +std::string tohex(const std::vector& v) { + static const char* H = "0123456789abcdef"; + std::string s; s.reserve(v.size() * 2); + for (unsigned char b : v) { s.push_back(H[b >> 4]); s.push_back(H[b & 0xf]); } + return s; +} + +// --- inputs shared verbatim with gentx_coinbase_test.cpp NOSEG vector -------- +const std::vector CB = unhex("03a1b2c3041122334455667788"); +const std::vector P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac"); +const std::vector P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac"); +const std::vector DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac"); + +const std::vector, uint64_t>> PAYOUTS = { + {P1, 5000000000ull}, + {P2, 2500000000ull}, +}; + +// ref_hash = 0xab * 32 ; last_txout_nonce LE bytes = 08 07 06 05 04 03 02 01. +const uint64_t NONCE = 0x0102030405060708ull; + +// Oracle ground truth (identical to gentx_coinbase_test.cpp NOSEG_BYTES). +const std::string NOSEG_BYTES = + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000"; + +dgb::coin::ConnCoinbaseParts build() { + dgb::coin::ConnCoinbaseInputs in; + in.coinbase_script = CB; + in.segwit_commitment_script = std::nullopt; + in.payout_outputs = PAYOUTS; + in.donation_amount = 1; // matches NOSEG donation value (1 sat) + in.donation_script = DON; + in.ref_hash = uint256(std::vector(32, 0xab)); + in.last_txout_nonce = NONCE; + return dgb::coin::build_connection_coinbase_parts(in); +} + +// (1) full assembled bytes == oracle wire. +TEST(ConnCoinbase, FullBytesMatchOracle) { + auto parts = build(); + EXPECT_EQ(tohex(parts.gentx.bytes), NOSEG_BYTES); +} + +// (2) the Stratum split reassembles to the exact oracle bytes with the +// 8-byte extranonce slot filling the OP_RETURN nonce. +TEST(ConnCoinbase, SplitReassemblesToOracle) { + auto parts = build(); + const std::string extranonce = "0807060504030201"; // en1(4)||en2(4), LE nonce + EXPECT_EQ(parts.coinb1 + extranonce + parts.coinb2, NOSEG_BYTES); +} + +// (3) coinb1 is the prefix up to (excl.) the nonce slot; coinb2 is the locktime. +TEST(ConnCoinbase, Coinb1PrefixCoinb2Locktime) { + auto parts = build(); + ASSERT_GE(NOSEG_BYTES.size(), size_t{24}); + // coinb1 == oracle minus the last 12 bytes (nonce 8 + locktime 4 = 24 hex) + EXPECT_EQ(parts.coinb1, NOSEG_BYTES.substr(0, NOSEG_BYTES.size() - 24)); + // coinb2 == final 4 bytes (locktime), all-zero + EXPECT_EQ(parts.coinb2, "00000000"); +} + +// (4) the OP_RETURN ref script is the canonical 6a28 || ref_hash || nonce(LE). +TEST(ConnCoinbase, RefOpReturnLayout) { + auto op = dgb::coin::build_ref_op_return( + uint256(std::vector(32, 0xab)), NONCE); + std::string ref32; for (int i = 0; i < 32; ++i) ref32 += "ab"; + EXPECT_EQ(tohex(op), std::string("6a28") + ref32 + "0807060504030201"); +} + +} // namespace