|
| 1 | +// DGB #82 / #172 — share-path gentx KAT. |
| 2 | +// |
| 3 | +// The helper-level KAT (gentx_coinbase_test) pins dgb::coin::assemble_gentx_coinbase |
| 4 | +// directly against oracle vectors. This test proves the *consumption* side of |
| 5 | +// the #172 collapse: that generate_share_transaction() — the verification SSOT |
| 6 | +// call site that #172 rewired onto assemble_gentx_coinbase — actually EMITS the |
| 7 | +// SSOT framing for a real share, not merely that the pure helper does. |
| 8 | +// |
| 9 | +// The OP_RETURN ref commitment is share-derived: |
| 10 | +// ref_hash = check_merkle_link(Hash(ref_serialization), m_ref_merkle_link) |
| 11 | +// so the share-path gentx cannot be pinned to a fixed 0xab..×32 oracle byte |
| 12 | +// vector (criterion 1: no oracle vector regeneration). Instead we assert a |
| 13 | +// round-trip equivalence: generate_share_transaction(share) == a verbatim |
| 14 | +// re-derivation of the same per-share inputs handed to the SAME SSOT assembler. |
| 15 | +// If the share path ever stops routing through the SSOT, or builds the coinbase |
| 16 | +// inputs differently, the two txids diverge and this fails — which is exactly |
| 17 | +// "the collapse is what's tested." |
| 18 | +// |
| 19 | +// A v36 MergedMiningShare with a NULL parent is used so the PPLNS walk is |
| 20 | +// skipped entirely (no tracker chain entries are touched): payout_outputs is |
| 21 | +// empty and donation_amount == subsidy. That isolates the coinbase framing + |
| 22 | +// op_return derivation, which is the SSOT contract under test. |
| 23 | +// |
| 24 | +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist |
| 25 | +// or it becomes a #143-style NOT_BUILT sentinel that reds master. |
| 26 | + |
| 27 | +#include <gtest/gtest.h> |
| 28 | + |
| 29 | +#include <impl/dgb/share.hpp> |
| 30 | +#include <impl/dgb/share_tracker.hpp> |
| 31 | +#include <impl/dgb/share_check.hpp> |
| 32 | +#include <impl/dgb/params.hpp> |
| 33 | +#include <impl/dgb/coin/gentx_coinbase.hpp> |
| 34 | + |
| 35 | +#include <core/pack.hpp> |
| 36 | +#include <core/hash.hpp> |
| 37 | +#include <core/uint256.hpp> |
| 38 | + |
| 39 | +#include <cstdint> |
| 40 | +#include <optional> |
| 41 | +#include <span> |
| 42 | +#include <utility> |
| 43 | +#include <vector> |
| 44 | + |
| 45 | +namespace { |
| 46 | + |
| 47 | +// A v36 share with a null parent => PPLNS skipped (empty payouts, donation == |
| 48 | +// subsidy). Coinbase script / nonce / timestamp / last_txout_nonce are set to |
| 49 | +// non-trivial values so the op_return ref derivation is exercised. |
| 50 | +dgb::MergedMiningShare make_v36_share() |
| 51 | +{ |
| 52 | + dgb::MergedMiningShare s; |
| 53 | + s.m_prev_hash.SetNull(); // null parent => PPLNS walk skipped |
| 54 | + s.m_coinbase.m_data = { 0x03, 0xa1, 0xb2, 0xc3, 0x04, 0x11, 0x22, 0x33, 0x44 }; |
| 55 | + s.m_nonce = 0x12345678; |
| 56 | + s.m_pubkey_hash.SetNull(); |
| 57 | + s.m_pubkey_type = 0; |
| 58 | + s.m_subsidy = 500000000ull; |
| 59 | + s.m_donation = 0; |
| 60 | + s.m_stale_info = dgb::none; |
| 61 | + s.m_desired_version = 36; |
| 62 | + s.m_segwit_data = std::nullopt; |
| 63 | + s.m_far_share_hash.SetNull(); |
| 64 | + s.m_max_bits = 0x1e0fffff; |
| 65 | + s.m_bits = 0x1e0fffff; |
| 66 | + s.m_timestamp = 1718700000; |
| 67 | + s.m_absheight = 1000; |
| 68 | + // m_abswork default-constructs to zero (uint128) |
| 69 | + s.m_merged_payout_hash.SetNull(); |
| 70 | + s.m_last_txout_nonce = 0x0001020304050607ull; |
| 71 | + // m_ref_merkle_link left empty => check_merkle_link returns Hash(ref) directly |
| 72 | + return s; |
| 73 | +} |
| 74 | + |
| 75 | +// Verbatim re-derivation of generate_share_transaction's per-share coinbase |
| 76 | +// inputs (v36 path), ending in the SAME SSOT assembler. Mirrors share_check.hpp |
| 77 | +// section 4/5 byte-for-byte via the identical pack primitives. |
| 78 | +template <typename ShareT> |
| 79 | +dgb::coin::GentxCoinbase rederive_via_ssot( |
| 80 | + const ShareT& share, const core::CoinParams& params) |
| 81 | +{ |
| 82 | + constexpr int64_t ver = ShareT::version; |
| 83 | + |
| 84 | + // null parent => empty payouts, donation_amount == subsidy |
| 85 | + std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs; |
| 86 | + uint64_t donation_amount = share.m_subsidy; |
| 87 | + |
| 88 | + // no segwit_data => no witness-commitment vout |
| 89 | + std::optional<std::vector<unsigned char>> segwit_commitment_script; |
| 90 | + |
| 91 | + auto donation_script = params.donation_script_func(ver); |
| 92 | + |
| 93 | + std::vector<unsigned char> op_return_script; |
| 94 | + { |
| 95 | + PackStream ref_stream; |
| 96 | + { |
| 97 | + auto hex = params.active_identifier_hex(); |
| 98 | + for (size_t i = 0; i + 1 < hex.size(); i += 2) |
| 99 | + { |
| 100 | + unsigned char byte = static_cast<unsigned char>( |
| 101 | + std::stoul(hex.substr(i, 2), nullptr, 16)); |
| 102 | + ref_stream.write(std::span<const std::byte>( |
| 103 | + reinterpret_cast<const std::byte*>(&byte), 1)); |
| 104 | + } |
| 105 | + } |
| 106 | + { |
| 107 | + ref_stream << share.m_prev_hash; |
| 108 | + ref_stream << share.m_coinbase; |
| 109 | + ref_stream << share.m_nonce; |
| 110 | + |
| 111 | + if constexpr (requires { share.m_address; }) |
| 112 | + ref_stream << share.m_address; |
| 113 | + else if constexpr (requires { share.m_pubkey_type; }) |
| 114 | + { |
| 115 | + ref_stream << share.m_pubkey_hash; |
| 116 | + ref_stream << share.m_pubkey_type; |
| 117 | + } |
| 118 | + else |
| 119 | + ref_stream << share.m_pubkey_hash; |
| 120 | + |
| 121 | + if constexpr (core::version_gate::is_v36_active(ver)) |
| 122 | + ::Serialize(ref_stream, VarInt(share.m_subsidy)); |
| 123 | + else |
| 124 | + ref_stream << share.m_subsidy; |
| 125 | + |
| 126 | + ref_stream << share.m_donation; |
| 127 | + { |
| 128 | + uint8_t si = static_cast<uint8_t>(share.m_stale_info); |
| 129 | + ref_stream << si; |
| 130 | + } |
| 131 | + ::Serialize(ref_stream, VarInt(share.m_desired_version)); |
| 132 | + |
| 133 | + if constexpr (requires { share.m_segwit_data; }) |
| 134 | + { |
| 135 | + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) |
| 136 | + { |
| 137 | + if (share.m_segwit_data.has_value()) { |
| 138 | + ref_stream << share.m_segwit_data.value(); |
| 139 | + } else { |
| 140 | + std::vector<uint256> empty_branch; |
| 141 | + ref_stream << empty_branch; |
| 142 | + uint256 zero_root; |
| 143 | + ref_stream << zero_root; |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + if constexpr (core::version_gate::is_v36_active(ver)) |
| 149 | + { |
| 150 | + if constexpr (requires { share.m_merged_addresses; }) |
| 151 | + ref_stream << share.m_merged_addresses; |
| 152 | + } |
| 153 | + |
| 154 | + if constexpr (ver < 34) |
| 155 | + { |
| 156 | + if constexpr (requires { share.m_tx_info; }) |
| 157 | + ref_stream << share.m_tx_info; |
| 158 | + } |
| 159 | + |
| 160 | + ref_stream << share.m_far_share_hash; |
| 161 | + ref_stream << share.m_max_bits; |
| 162 | + ref_stream << share.m_bits; |
| 163 | + ref_stream << share.m_timestamp; |
| 164 | + ref_stream << share.m_absheight; |
| 165 | + |
| 166 | + if constexpr (core::version_gate::is_v36_active(ver)) |
| 167 | + { |
| 168 | + if constexpr (requires { share.m_abswork; }) |
| 169 | + ::Serialize(ref_stream, Using<dgb::AbsworkV36Format>(share.m_abswork)); |
| 170 | + } |
| 171 | + else |
| 172 | + { |
| 173 | + ref_stream << share.m_abswork; |
| 174 | + } |
| 175 | + |
| 176 | + if constexpr (core::version_gate::is_v36_active(ver)) |
| 177 | + { |
| 178 | + if constexpr (requires { share.m_merged_coinbase_info; }) |
| 179 | + ref_stream << share.m_merged_coinbase_info; |
| 180 | + if constexpr (requires { share.m_merged_payout_hash; }) |
| 181 | + ref_stream << share.m_merged_payout_hash; |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + if constexpr (core::version_gate::is_v36_active(ver)) |
| 186 | + { |
| 187 | + if constexpr (requires { share.m_message_data; }) |
| 188 | + ref_stream << share.m_message_data; |
| 189 | + } |
| 190 | + |
| 191 | + auto ref_span = std::span<const unsigned char>( |
| 192 | + reinterpret_cast<const unsigned char*>(ref_stream.data()), ref_stream.size()); |
| 193 | + uint256 hash_ref = Hash(ref_span); |
| 194 | + uint256 ref_hash = dgb::check_merkle_link(hash_ref, share.m_ref_merkle_link); |
| 195 | + |
| 196 | + op_return_script.push_back(0x6a); // OP_RETURN |
| 197 | + op_return_script.push_back(0x28); // PUSH 40 |
| 198 | + op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); |
| 199 | + { |
| 200 | + uint64_t nonce = share.m_last_txout_nonce; |
| 201 | + auto* p = reinterpret_cast<const unsigned char*>(&nonce); |
| 202 | + op_return_script.insert(op_return_script.end(), p, p + 8); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + return dgb::coin::assemble_gentx_coinbase( |
| 207 | + share.m_coinbase.m_data, segwit_commitment_script, payout_outputs, |
| 208 | + donation_amount, donation_script, op_return_script); |
| 209 | +} |
| 210 | + |
| 211 | +// --- Test 1: share path emits SSOT framing (round-trip equivalence) ----------- |
| 212 | +TEST(DgbGentxSharePath, GenerateShareTransactionEmitsSsotFraming) |
| 213 | +{ |
| 214 | + auto params = dgb::make_coin_params(/*testnet=*/false); |
| 215 | + dgb::ShareTracker tracker; // empty chain |
| 216 | + auto share = make_v36_share(); |
| 217 | + |
| 218 | + // Route A: the verification SSOT call site under test. |
| 219 | + uint256 actual = dgb::generate_share_transaction( |
| 220 | + share, tracker, params, /*dump_diag=*/false, /*v36_active=*/true); |
| 221 | + |
| 222 | + // Route B: verbatim re-derivation through the SAME assembler. |
| 223 | + auto expected = rederive_via_ssot(share, params); |
| 224 | + |
| 225 | + EXPECT_EQ(actual, expected.txid) |
| 226 | + << "generate_share_transaction no longer emits assemble_gentx_coinbase framing"; |
| 227 | +} |
| 228 | + |
| 229 | +// --- Test 2: determinism (the SSOT path is a pure function of the share) ------- |
| 230 | +TEST(DgbGentxSharePath, Deterministic) |
| 231 | +{ |
| 232 | + auto params = dgb::make_coin_params(false); |
| 233 | + dgb::ShareTracker tracker; |
| 234 | + auto share = make_v36_share(); |
| 235 | + |
| 236 | + uint256 a = dgb::generate_share_transaction(share, tracker, params, false, true); |
| 237 | + uint256 b = dgb::generate_share_transaction(share, tracker, params, false, true); |
| 238 | + EXPECT_EQ(a, b); |
| 239 | +} |
| 240 | + |
| 241 | +// --- Test 3: coinbase-affecting fields flow into the SSOT gentx ---------------- |
| 242 | +// last_txout_nonce feeds the op_return; coinbase script feeds vin[0]. Changing |
| 243 | +// either must change the gentx txid (proves the share data reaches the SSOT, |
| 244 | +// not a cached/stubbed value). |
| 245 | +TEST(DgbGentxSharePath, CoinbaseInputsAreLoadBearing) |
| 246 | +{ |
| 247 | + auto params = dgb::make_coin_params(false); |
| 248 | + dgb::ShareTracker tracker; |
| 249 | + |
| 250 | + auto base = make_v36_share(); |
| 251 | + uint256 h0 = dgb::generate_share_transaction(base, tracker, params, false, true); |
| 252 | + |
| 253 | + auto bumped_nonce = base; |
| 254 | + bumped_nonce.m_last_txout_nonce ^= 0xffull; |
| 255 | + uint256 h1 = dgb::generate_share_transaction(bumped_nonce, tracker, params, false, true); |
| 256 | + EXPECT_NE(h0, h1) << "last_txout_nonce did not reach the op_return commitment"; |
| 257 | + |
| 258 | + auto bumped_cb = base; |
| 259 | + bumped_cb.m_coinbase.m_data.push_back(0x99); |
| 260 | + uint256 h2 = dgb::generate_share_transaction(bumped_cb, tracker, params, false, true); |
| 261 | + EXPECT_NE(h0, h2) << "coinbase script did not reach the gentx vin"; |
| 262 | +} |
| 263 | + |
| 264 | +} // namespace |
0 commit comments