From 2535874573bcee70279e3f281994d74f17ce1f4d Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 21:29:16 +0000 Subject: [PATCH 1/3] dgb(#82): collapse inline gentx serializers onto SSOT assemble_gentx_coinbase generate_share_transaction (verification path) and create_local_share (emission path) each open-coded the coinbase wire layout (version|vin|vouts|locktime) and the txid double-SHA256. Two byte paths pinned to the same oracle vector = proven-equivalent, not unified: a third edit could still diverge them. Collapse both onto dgb::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp, the #171 SSOT) so emission == verification == one source. Each call site now only builds its per-share inputs (coinbase script, optional segwit witness-commitment script, PPLNS payout outputs, donation, OP_RETURN ref commitment) and hands them to the assembler; the assembler owns wire framing + txid. PackStream tx is reconstructed from the SSOT bytes so the downstream V36 hash_link diagnostics are byte-for-byte unchanged. No oracle vector regeneration. Single-coin scope: src/impl/dgb/ only. dgb build EXIT=0; dgb_share_test 5/5; dgb_gentx_coinbase_test 3/3. --- src/impl/dgb/share_check.hpp | 166 ++++++++++------------------------- 1 file changed, 46 insertions(+), 120 deletions(-) diff --git a/src/impl/dgb/share_check.hpp b/src/impl/dgb/share_check.hpp index 622f3b51f..467a727da 100644 --- a/src/impl/dgb/share_check.hpp +++ b/src/impl/dgb/share_check.hpp @@ -8,6 +8,8 @@ #include "share_messages.hpp" #include "share_types.hpp" +#include + #include #include #include @@ -1133,81 +1135,22 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS); // --- 4. Serialise the coinbase transaction --- - // Non-witness serialization (for txid computation): - // version(4) + vin_count(varint) + vin + vout_count(varint) + vouts + locktime(4) - PackStream tx; - - // tx version = 1 - uint32_t tx_version = 1; - tx.write(std::span(reinterpret_cast(&tx_version), 4)); - - // vin count = 1 - { - unsigned char one = 1; - tx.write(std::span(reinterpret_cast(&one), 1)); - } - - // vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0 - { - // prev_hash (32 zero bytes) - uint256 zero_hash; - tx << zero_hash; - // prev_index (0xffffffff) - uint32_t prev_idx = 0xffffffff; - tx.write(std::span(reinterpret_cast(&prev_idx), 4)); - // script (VarStr) - tx << share.m_coinbase; - // sequence (0xffffffff — standard coinbase sequence, matches Python) - uint32_t seq = 0xffffffff; - tx.write(std::span(reinterpret_cast(&seq), 4)); - } - - // Count total outputs - size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN commitment */; - // Segwit commitment output (if applicable) - bool has_segwit = false; + // Wire layout (version|vin|vouts|locktime) and txid are produced by the + // SSOT assembler dgb::coin::assemble_gentx_coinbase() so that emission and + // verification can never diverge on a byte. This function only builds the + // per-share inputs (coinbase script, optional segwit commitment, payout + // outputs, donation, OP_RETURN ref commitment). + + // Segwit witness-commitment output (value=0) — present iff this share + // carries segwit data on a segwit-active version. The script is + // 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || P2POOL_WITNESS_NONCE). + std::optional> segwit_commitment_script; if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) { if constexpr (requires { share.m_segwit_data; }) { if (share.m_segwit_data.has_value()) { - has_segwit = true; - n_outs += 1; - } - } - } - - // vout count (varint — for < 253 outputs, it's a single byte) - if (n_outs < 253) - { - uint8_t cnt = static_cast(n_outs); - tx.write(std::span(reinterpret_cast(&cnt), 1)); - } - else - { - uint8_t marker = 0xfd; - tx.write(std::span(reinterpret_cast(&marker), 1)); - uint16_t cnt = static_cast(n_outs); - tx.write(std::span(reinterpret_cast(&cnt), 2)); - } - - // Helper to write a single tx_out: value(8LE) + script(VarStr) - auto write_txout = [&](uint64_t value, const std::vector& script) { - tx.write(std::span(reinterpret_cast(&value), 8)); - BaseScript bs; - bs.m_data = script; - tx << bs; - }; - - // Segwit commitment output (value=0, script=OP_RETURN + witness_commitment) - if (has_segwit) - { - if constexpr (requires { share.m_segwit_data; }) - { - if (share.m_segwit_data.has_value()) - { - // witness commitment: 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || '[P2Pool]'*4) std::vector wscript; wscript.push_back(0x6a); // OP_RETURN wscript.push_back(0x24); // PUSH 36 @@ -1219,7 +1162,7 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); auto commitment_bytes = commitment.GetChars(); wscript.insert(wscript.end(), commitment_bytes.begin(), commitment_bytes.end()); - write_txout(0, wscript); + segwit_commitment_script = std::move(wscript); if (dump_diag) { LOG_INFO << "[WC-GST] wtxid_root=" << sd.m_wtxid_merkle_root.GetHex() << " commitment=" << commitment.GetHex(); @@ -1228,17 +1171,13 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const } } - // PPLNS payout outputs - for (auto& [script, amount] : payout_outputs) - write_txout(amount, script); - // Donation output — V35 shares always use P2PK DONATION_SCRIPT, // V36 shares always use P2SH COMBINED_DONATION_SCRIPT. // Each share was created with the donation script matching its version. auto donation_script = params.donation_script_func(ver); - write_txout(donation_amount, donation_script); // OP_RETURN commitment: value=0, script = 0x6a28 + ref_hash(32) + last_txout_nonce(8) + std::vector op_return_script; { // We need the ref_hash — recompute it from the share the same way share_init_verify does PackStream ref_stream; @@ -1350,7 +1289,6 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); // Build OP_RETURN script: 0x6a 0x28 + ref_hash(32) + last_txout_nonce(8) - std::vector op_return_script; op_return_script.push_back(0x6a); // OP_RETURN op_return_script.push_back(0x28); // PUSH 40 bytes op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); @@ -1359,19 +1297,18 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const auto* p = reinterpret_cast(&nonce); op_return_script.insert(op_return_script.end(), p, p + 8); } - write_txout(0, op_return_script); - } - - // lock_time = 0 - { - uint32_t locktime = 0; - tx.write(std::span(reinterpret_cast(&locktime), 4)); } - // --- 5. Compute txid (double-SHA256 of non-witness serialization) --- - auto tx_span = std::span( - reinterpret_cast(tx.data()), tx.size()); - auto txid = Hash(tx_span); + // --- 5. Assemble + compute txid via the SSOT (double-SHA256 of + // non-witness serialization). Reconstruct the PackStream tx so the + // downstream V36 hash_link diagnostics keep working unchanged. + auto _gc = dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_commitment_script, payout_outputs, + donation_amount, donation_script, op_return_script); + PackStream tx; + tx.write(std::span( + reinterpret_cast(_gc.bytes.data()), _gc.bytes.size())); + auto txid = _gc.txid; // V36 hash_link cross-check: compute prefix hash_link from our coinbase // and compare with the share's stored hash_link. If states differ, @@ -1452,8 +1389,8 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const LOG_WARNING << "[GENTX-DIAG] coinbase_hex=" << to_hex(cp, tx.size()); LOG_WARNING << "[GENTX-DIAG] pplns_outputs=" << payout_outputs.size() << " donation_amount=" << donation_amount - << " n_outs=" << n_outs - << " has_segwit=" << has_segwit; + << " n_outs=" << (payout_outputs.size() + 2 + (segwit_commitment_script.has_value() ? 1u : 0u)) + << " has_segwit=" << segwit_commitment_script.has_value(); LOG_WARNING << "[GENTX-DIAG] total_weight=" << total_weight.GetHex() << " total_don_weight=" << total_donation_weight.GetHex(); for (size_t i = 0; i < payout_outputs.size(); ++i) { @@ -2729,7 +2666,8 @@ uint256 create_local_share( share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ (static_cast(min_header.m_nonce) << 32); - // --- Build the p2pool coinbase in the same format as generate_share_transaction --- + // --- Build the p2pool coinbase via the SSOT assemble_gentx_coinbase() --- + // (identical wire layout to generate_share_transaction; one byte path, not a twin) // This is needed to compute hash_link and to verify the share locally. // The coinbase format is: version(4) + vin(1 input) + vout(outputs...) + locktime(4) // @@ -2942,41 +2880,29 @@ uint256 create_local_share( if (payout_outputs.size() > 4000) payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); - PackStream gentx; - { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } - { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } - { uint256 z; gentx << z; } - { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } - gentx << share.m_coinbase; - { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } - size_t n_outs = payout_outputs.size() + 1 + 1; - bool has_segwit_fb = share.m_segwit_data.has_value(); - if (has_segwit_fb) n_outs += 1; - if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } - else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } - auto write_txout = [&](uint64_t value, const std::vector& script) { - gentx.write(std::span(reinterpret_cast(&value), 8)); - BaseScript bs; bs.m_data = script; gentx << bs; - }; - if (has_segwit_fb) { + // Build the p2pool coinbase via the SSOT assembler + // dgb::coin::assemble_gentx_coinbase() — identical wire layout to + // generate_share_transaction(). This is no longer a hand-rolled twin: + // both emission and verification route through the one byte path. + std::optional> segwit_opt; + if (share.m_segwit_data.has_value()) { auto& sd = share.m_segwit_data.value(); std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); auto cb = commitment.GetChars(); wscript.insert(wscript.end(), cb.begin(), cb.end()); - write_txout(0, wscript); - } - for (auto& [script, amount] : payout_outputs) write_txout(amount, script); - write_txout(donation_amount, params.donation_script_func(int64_t(36))); - { std::vector op; op.push_back(0x6a); op.push_back(0x28); - op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); - uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); - op.insert(op.end(), p, p + 8); write_txout(0, op); } - { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } - - coinbase_bytes_for_hashlink.assign( - reinterpret_cast(gentx.data()), - reinterpret_cast(gentx.data()) + gentx.size()); + segwit_opt = std::move(wscript); + } + auto donation_script = params.donation_script_func(int64_t(36)); + std::vector op_return_vec; + op_return_vec.push_back(0x6a); op_return_vec.push_back(0x28); + op_return_vec.insert(op_return_vec.end(), ref_hash.data(), ref_hash.data() + 32); + { uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op_return_vec.insert(op_return_vec.end(), p, p + 8); } + auto _gc = dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_opt, payout_outputs, + donation_amount, donation_script, op_return_vec); + coinbase_bytes_for_hashlink = _gc.bytes; } // The split point: everything before ref_hash + last_txout_nonce + locktime From cadec972d0283db5bedaa95d714accbb8faf61ef Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 21:59:57 +0000 Subject: [PATCH 2/3] =?UTF-8?q?dgb(#172):=20share-path=20gentx=20KAT=20?= =?UTF-8?q?=E2=80=94=20prove=20generate=5Fshare=5Ftransaction=20emits=20SS?= =?UTF-8?q?OT=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-trip equivalence KAT (op_return ref_hash is share-derived, cannot pin a fixed oracle vector): generate_share_transaction(share) txid == verbatim re-derivation through the SAME assemble_gentx_coinbase SSOT. Proves the #172 collapse behaviorally, not structurally. 3/3 PASS, txid 9e2b78d5. Wired into ctest + BOTH build.yml allowlists in this commit (#143 NOT_BUILT trap). --- .github/workflows/build.yml | 8 +- src/impl/dgb/test/CMakeLists.txt | 14 ++ src/impl/dgb/test/gentx_share_path_test.cpp | 264 ++++++++++++++++++++ 3 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/impl/dgb/test/gentx_share_path_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 297d7891a..decda14e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,8 +67,8 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \ - nmc_auxpow_merkle_test \ + test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -198,8 +198,8 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \ - nmc_auxpow_merkle_test \ + test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 26ee111be..63a082a16 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -54,6 +54,20 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_gentx_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 + # a round-trip equivalence (the op_return ref_hash is share-derived, so it + # cannot be pinned to a fixed oracle vector). Links the dgb OBJECT lib like + # dgb_share_test. MUST also be in the build.yml --target allowlist (#143). + add_executable(dgb_gentx_share_path_test gentx_share_path_test.cpp) + target_link_libraries(dgb_gentx_share_path_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_gentx_share_path_test "" AUTO) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) target_link_libraries(${dgb_guard} PRIVATE diff --git a/src/impl/dgb/test/gentx_share_path_test.cpp b/src/impl/dgb/test/gentx_share_path_test.cpp new file mode 100644 index 000000000..e1a3e4824 --- /dev/null +++ b/src/impl/dgb/test/gentx_share_path_test.cpp @@ -0,0 +1,264 @@ +// DGB #82 / #172 — share-path gentx KAT. +// +// The helper-level KAT (gentx_coinbase_test) pins dgb::coin::assemble_gentx_coinbase +// directly against oracle vectors. This test proves the *consumption* side of +// the #172 collapse: that generate_share_transaction() — the verification SSOT +// call site that #172 rewired onto assemble_gentx_coinbase — actually EMITS the +// SSOT framing for a real share, not merely that the pure helper does. +// +// The OP_RETURN ref commitment is share-derived: +// ref_hash = check_merkle_link(Hash(ref_serialization), m_ref_merkle_link) +// so the share-path gentx cannot be pinned to a fixed 0xab..×32 oracle byte +// vector (criterion 1: no oracle vector regeneration). Instead we assert a +// round-trip equivalence: generate_share_transaction(share) == a verbatim +// re-derivation of the same per-share inputs handed to the SAME SSOT assembler. +// If the share path ever stops routing through the SSOT, or builds the coinbase +// inputs differently, the two txids diverge and this fails — which is exactly +// "the collapse is what's tested." +// +// A v36 MergedMiningShare with a NULL parent is used so the PPLNS walk is +// skipped entirely (no tracker chain entries are touched): payout_outputs is +// empty and donation_amount == subsidy. That isolates the coinbase framing + +// op_return derivation, which is the SSOT contract under test. +// +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist +// or it becomes a #143-style NOT_BUILT sentinel that reds master. + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +// A v36 share with a null parent => PPLNS skipped (empty payouts, donation == +// subsidy). Coinbase script / nonce / timestamp / last_txout_nonce are set to +// non-trivial values so the op_return ref derivation is exercised. +dgb::MergedMiningShare make_v36_share() +{ + dgb::MergedMiningShare s; + s.m_prev_hash.SetNull(); // null parent => PPLNS walk skipped + s.m_coinbase.m_data = { 0x03, 0xa1, 0xb2, 0xc3, 0x04, 0x11, 0x22, 0x33, 0x44 }; + s.m_nonce = 0x12345678; + s.m_pubkey_hash.SetNull(); + s.m_pubkey_type = 0; + s.m_subsidy = 500000000ull; + s.m_donation = 0; + s.m_stale_info = dgb::none; + s.m_desired_version = 36; + s.m_segwit_data = std::nullopt; + s.m_far_share_hash.SetNull(); + s.m_max_bits = 0x1e0fffff; + s.m_bits = 0x1e0fffff; + s.m_timestamp = 1718700000; + s.m_absheight = 1000; + // m_abswork default-constructs to zero (uint128) + s.m_merged_payout_hash.SetNull(); + s.m_last_txout_nonce = 0x0001020304050607ull; + // m_ref_merkle_link left empty => check_merkle_link returns Hash(ref) directly + return s; +} + +// Verbatim re-derivation of generate_share_transaction's per-share coinbase +// inputs (v36 path), ending in the SAME SSOT assembler. Mirrors share_check.hpp +// section 4/5 byte-for-byte via the identical pack primitives. +template +dgb::coin::GentxCoinbase rederive_via_ssot( + const ShareT& share, const core::CoinParams& params) +{ + constexpr int64_t ver = ShareT::version; + + // null parent => empty payouts, donation_amount == subsidy + std::vector, uint64_t>> payout_outputs; + uint64_t donation_amount = share.m_subsidy; + + // no segwit_data => no witness-commitment vout + std::optional> segwit_commitment_script; + + auto donation_script = params.donation_script_func(ver); + + std::vector op_return_script; + { + PackStream ref_stream; + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (core::version_gate::is_v36_active(ver)) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + uint256 ref_hash = dgb::check_merkle_link(hash_ref, share.m_ref_merkle_link); + + op_return_script.push_back(0x6a); // OP_RETURN + op_return_script.push_back(0x28); // PUSH 40 + op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); + { + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + op_return_script.insert(op_return_script.end(), p, p + 8); + } + } + + return dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_commitment_script, payout_outputs, + donation_amount, donation_script, op_return_script); +} + +// --- Test 1: share path emits SSOT framing (round-trip equivalence) ----------- +TEST(DgbGentxSharePath, GenerateShareTransactionEmitsSsotFraming) +{ + auto params = dgb::make_coin_params(/*testnet=*/false); + dgb::ShareTracker tracker; // empty chain + auto share = make_v36_share(); + + // Route A: the verification SSOT call site under test. + uint256 actual = dgb::generate_share_transaction( + share, tracker, params, /*dump_diag=*/false, /*v36_active=*/true); + + // Route B: verbatim re-derivation through the SAME assembler. + auto expected = rederive_via_ssot(share, params); + + EXPECT_EQ(actual, expected.txid) + << "generate_share_transaction no longer emits assemble_gentx_coinbase framing"; +} + +// --- Test 2: determinism (the SSOT path is a pure function of the share) ------- +TEST(DgbGentxSharePath, Deterministic) +{ + auto params = dgb::make_coin_params(false); + dgb::ShareTracker tracker; + auto share = make_v36_share(); + + uint256 a = dgb::generate_share_transaction(share, tracker, params, false, true); + uint256 b = dgb::generate_share_transaction(share, tracker, params, false, true); + EXPECT_EQ(a, b); +} + +// --- Test 3: coinbase-affecting fields flow into the SSOT gentx ---------------- +// last_txout_nonce feeds the op_return; coinbase script feeds vin[0]. Changing +// either must change the gentx txid (proves the share data reaches the SSOT, +// not a cached/stubbed value). +TEST(DgbGentxSharePath, CoinbaseInputsAreLoadBearing) +{ + auto params = dgb::make_coin_params(false); + dgb::ShareTracker tracker; + + auto base = make_v36_share(); + uint256 h0 = dgb::generate_share_transaction(base, tracker, params, false, true); + + auto bumped_nonce = base; + bumped_nonce.m_last_txout_nonce ^= 0xffull; + uint256 h1 = dgb::generate_share_transaction(bumped_nonce, tracker, params, false, true); + EXPECT_NE(h0, h1) << "last_txout_nonce did not reach the op_return commitment"; + + auto bumped_cb = base; + bumped_cb.m_coinbase.m_data.push_back(0x99); + uint256 h2 = dgb::generate_share_transaction(bumped_cb, tracker, params, false, true); + EXPECT_NE(h0, h2) << "coinbase script did not reach the gentx vin"; +} + +} // namespace From 7b1f57a4a9888f813614ded8fee8fbfbf726ccd6 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 22:37:17 +0000 Subject: [PATCH 3/3] dgb(#82): expose SSOT gentx bytes for won-block reconstruction generate_share_transaction computes the coinbase via assemble_gentx_coinbase (the SSOT proven by #172) but returned only the gentx_hash, discarding the bytes. reconstruct_won_block needs the exact non-witness coinbase bytes -- it is block tx[0] and its txid is the merkle leaf -- not just the hash. Add an optional out_gentx (GentxCoinbase*) param, default nullptr: zero change for the verification callers, surfaces {bytes,txid} for the reconstructor. KAT proves the exposed bytes hash to the returned gentx_hash and deserialize into the coinbase MutableTransaction that assemble_won_block frames, round-tripping byte-identically (non-witness). Single-coin (src/impl/dgb/ only); reuses the existing #172 dgb_gentx_share_path_test target (no new allowlist entry). --- src/impl/dgb/share_check.hpp | 7 +++- src/impl/dgb/test/gentx_share_path_test.cpp | 44 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/impl/dgb/share_check.hpp b/src/impl/dgb/share_check.hpp index 467a727da..d3ec92ffa 100644 --- a/src/impl/dgb/share_check.hpp +++ b/src/impl/dgb/share_check.hpp @@ -933,7 +933,7 @@ inline std::vector get_share_script(const auto* obj) // Reference: frstrtr/p2pool-merged-v36 p2pool/data.py generate_transaction() // ============================================================================ template -uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const core::CoinParams& params, bool dump_diag = false, bool v36_active = false) +uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const core::CoinParams& params, bool dump_diag = false, bool v36_active = false, dgb::coin::GentxCoinbase* out_gentx = nullptr) { auto gst_t0 = std::chrono::steady_clock::now(); constexpr int64_t ver = ShareT::version; @@ -1310,6 +1310,11 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const reinterpret_cast(_gc.bytes.data()), _gc.bytes.size())); auto txid = _gc.txid; + // Expose the SSOT gentx (non-witness bytes + txid) for the won-block + // reconstructor (#82): block tx[0] is this coinbase and gentx_hash is + // its merkle leaf. Default nullptr => zero change for verification callers. + if (out_gentx) *out_gentx = _gc; + // V36 hash_link cross-check: compute prefix hash_link from our coinbase // and compare with the share's stored hash_link. If states differ, // the prefix (outputs/amounts) differs from what p2pool built. diff --git a/src/impl/dgb/test/gentx_share_path_test.cpp b/src/impl/dgb/test/gentx_share_path_test.cpp index e1a3e4824..f1c91da35 100644 --- a/src/impl/dgb/test/gentx_share_path_test.cpp +++ b/src/impl/dgb/test/gentx_share_path_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -261,4 +262,47 @@ TEST(DgbGentxSharePath, CoinbaseInputsAreLoadBearing) EXPECT_NE(h0, h2) << "coinbase script did not reach the gentx vin"; } +// --- Test 4: SSOT gentx bytes are exposed for won-block reconstruction (#82) --- +// reconstruct_won_block needs the EXACT coinbase bytes whose txid is the merkle +// leaf -- not just the hash. The out_gentx param surfaces them straight from the +// SSOT assembler; they must (a) hash to the returned gentx_hash and (b) +// deserialize into the coinbase MutableTransaction that assemble_won_block frames +// as block tx[0], round-tripping byte-identically. +TEST(DgbGentxSharePath, SsotGentxBytesExposedForReconstruct) +{ + auto params = dgb::make_coin_params(false); + dgb::ShareTracker tracker; + auto share = make_v36_share(); + + dgb::coin::GentxCoinbase gc; + uint256 gentx_hash = dgb::generate_share_transaction( + share, tracker, params, /*dump_diag=*/false, /*v36_active=*/true, &gc); + + ASSERT_FALSE(gc.bytes.empty()) << "out_gentx not populated"; + EXPECT_EQ(gc.txid, gentx_hash) + << "exposed gentx bytes do not match the returned gentx_hash"; + auto sp = std::span(gc.bytes.data(), gc.bytes.size()); + EXPECT_EQ(Hash(sp), gentx_hash) + << "double-SHA(exposed bytes) != gentx_hash (merkle leaf would mismatch)"; + + // Deserialize into the coinbase tx that assemble_won_block consumes. + dgb::coin::MutableTransaction gentx; + { + PackStream ps(gc.bytes); + dgb::coin::UnserializeTransaction(gentx, ps, dgb::coin::TX_NO_WITNESS); + } + // Coinbase shape: single input spending the null outpoint at 0xffffffff. + ASSERT_EQ(gentx.vin.size(), 1u); + EXPECT_TRUE(gentx.vin[0].prevout.hash.IsNull()); + EXPECT_EQ(gentx.vin[0].prevout.index, 0xffffffffu); + + // Re-serialize non-witness => byte-identical to the SSOT bytes, so the + // reconstructed block tx[0] is faithful and the daemon-side block hashes. + auto repacked = pack(dgb::coin::TX_NO_WITNESS(gentx)); + std::vector rebytes( + reinterpret_cast(repacked.data()), + reinterpret_cast(repacked.data()) + repacked.size()); + EXPECT_EQ(rebytes, gc.bytes) << "coinbase tx does not round-trip the SSOT bytes"; +} + } // namespace