Skip to content

Commit cadec97

Browse files
committed
dgb(#172): share-path gentx KAT — prove generate_share_transaction emits SSOT framing
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).
1 parent 2535874 commit cadec97

3 files changed

Lines changed: 282 additions & 4 deletions

File tree

.github/workflows/build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ jobs:
6767
test_phase4_embedded \
6868
test_mweb_builder \
6969
test_address_resolution test_compute_share_target \
70-
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
71-
nmc_auxpow_merkle_test \
70+
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
71+
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \
7272
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7373
v37_test \
7474
-j$(nproc)
@@ -198,8 +198,8 @@ jobs:
198198
test_phase4_embedded \
199199
test_mweb_builder \
200200
test_address_resolution test_compute_share_target \
201-
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
202-
nmc_auxpow_merkle_test \
201+
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
202+
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \
203203
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
204204
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
205205
v37_test \

src/impl/dgb/test/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ if (BUILD_TESTING AND GTest_FOUND)
5454
dgb_coin pool sharechain)
5555
gtest_add_tests(dgb_gentx_coinbase_test "" AUTO)
5656

57+
# --- #172 share-path gentx KAT (collapse consumption proof) ------------
58+
# Proves generate_share_transaction (the verification SSOT call site #172
59+
# rewired) EMITS assemble_gentx_coinbase framing for a real v36 share, via
60+
# a round-trip equivalence (the op_return ref_hash is share-derived, so it
61+
# cannot be pinned to a fixed oracle vector). Links the dgb OBJECT lib like
62+
# dgb_share_test. MUST also be in the build.yml --target allowlist (#143).
63+
add_executable(dgb_gentx_share_path_test gentx_share_path_test.cpp)
64+
target_link_libraries(dgb_gentx_share_path_test PRIVATE
65+
GTest::gtest_main GTest::gtest
66+
core dgb
67+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
68+
dgb_coin pool sharechain)
69+
gtest_add_tests(dgb_gentx_share_path_test "" AUTO)
70+
5771
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test)
5872
add_executable(${dgb_guard} ${dgb_guard}.cpp)
5973
target_link_libraries(${dgb_guard} PRIVATE
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)