Skip to content

Commit 5089c6e

Browse files
committed
dgb(#82): extract SSOT gentx coinbase assembler + oracle-pinned round-trip KAT
Factor the p2pool non-witness coinbase (gentx) wire layout out of generate_share_transaction() (share_check.hpp:933, the verification SSOT) into coin/gentx_coinbase.hpp::assemble_gentx_coinbase() so emission and verification cannot diverge on a byte. Pure: takes already-built scripts/amounts (no tracker/share-template dependency) so it is directly KAT-able; this is the single builder that create_local_share() and the won-block reconstruction body will consume in follow-up slices. KAT vectors are GROUND-TRUTH from the canonical oracle frstrtr/p2pool-dgb-scrypt (util/pack.py byte logic + bitcoin/data.py tx_id_type; donation script 4104ffd0...ac), not self-generated, so a PASS proves build -> non-witness serialize -> double-SHA256 == oracle bytes/txid, not mere self-consistency. Covers the no-segwit and witness-commitment-first layouts; the only DGB<->BCH divergence is the gated segwit predicate at serialize time. dgb_gentx_coinbase_test 3/3 PASS, build EXIT=0; target wired into ctest + BOTH build.yml --target allowlists (#143 NOT_BUILT trap avoided). Scope: factor + KAT only. The won-block-reaches-network wiring (m_on_block_found and submit_block_hex) is fix #82 and stays separate.
1 parent 0989175 commit 5089c6e

4 files changed

Lines changed: 251 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ 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 \
70+
test_utxo test_dgb_subsidy dgb_share_test dgb_gentx_coinbase_test \
7171
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7272
v37_test \
7373
-j$(nproc)
@@ -197,7 +197,7 @@ jobs:
197197
test_phase4_embedded \
198198
test_mweb_builder \
199199
test_address_resolution test_compute_share_target \
200-
test_utxo test_dgb_subsidy dgb_share_test \
200+
test_utxo test_dgb_subsidy dgb_share_test dgb_gentx_coinbase_test \
201201
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
202202
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
203203
v37_test \
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#pragma once
2+
// ============================================================================
3+
// gentx_coinbase.hpp — SSOT non-witness coinbase (gentx) assembler.
4+
//
5+
// Single source of the p2pool coinbase wire layout. Consumed by:
6+
// - share_check.hpp generate_share_transaction() (the verification SSOT)
7+
// - share_check.hpp create_local_share() (the "same format as" smell)
8+
// - won-block reconstruction (as_block framing)
9+
// so that emission and verification can never diverge on a byte.
10+
//
11+
// Produces the NON-WITNESS serialization and its double-SHA256 txid
12+
// (== p2pool gentx_hash). Byte layout mirrors
13+
// frstrtr/p2pool-merged-v36 data.py generate_transaction():
14+
//
15+
// version(4 LE = 1)
16+
// vin_count(varint = 1)
17+
// vin[0] = prev_hash(32 zero) | prev_idx(0xffffffff) | script(VarStr) | seq(0xffffffff)
18+
// vout_count(varint)
19+
// vouts: [segwit_commitment?] ++ payout_outputs ++ donation ++ op_return_commitment
20+
// each vout = value(8 LE) | script(VarStr)
21+
// lock_time(4 = 0)
22+
//
23+
// Pure: takes already-built script/amount inputs (no tracker, no share template
24+
// dependency) so it is directly KAT-able against a canonical oracle vector.
25+
// ============================================================================
26+
27+
#include <core/pack.hpp>
28+
#include <core/pack_types.hpp>
29+
#include <core/hash.hpp>
30+
#include <core/uint256.hpp>
31+
32+
#include <cstdint>
33+
#include <optional>
34+
#include <span>
35+
#include <utility>
36+
#include <vector>
37+
38+
namespace dgb::coin
39+
{
40+
41+
struct GentxCoinbase
42+
{
43+
std::vector<unsigned char> bytes; // non-witness serialization
44+
uint256 txid; // double-SHA256(bytes) == gentx_hash
45+
};
46+
47+
// payout_outputs: (scriptPubKey, value) pairs in final consensus order.
48+
// segwit_commitment_script / segwit absent -> no witness-commitment vout.
49+
inline GentxCoinbase assemble_gentx_coinbase(
50+
const std::vector<unsigned char>& coinbase_script,
51+
const std::optional<std::vector<unsigned char>>& segwit_commitment_script,
52+
const std::vector<std::pair<std::vector<unsigned char>, uint64_t>>& payout_outputs,
53+
uint64_t donation_amount,
54+
const std::vector<unsigned char>& donation_script,
55+
const std::vector<unsigned char>& op_return_script)
56+
{
57+
PackStream tx;
58+
59+
// tx version = 1
60+
uint32_t tx_version = 1;
61+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&tx_version), 4));
62+
63+
// vin count = 1
64+
{
65+
unsigned char one = 1;
66+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&one), 1));
67+
}
68+
69+
// vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0xffffffff
70+
{
71+
uint256 zero_hash;
72+
tx << zero_hash;
73+
uint32_t prev_idx = 0xffffffff;
74+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&prev_idx), 4));
75+
BaseScript cb; cb.m_data = coinbase_script;
76+
tx << cb;
77+
uint32_t seq = 0xffffffff;
78+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&seq), 4));
79+
}
80+
81+
// vout count
82+
size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN */
83+
+ (segwit_commitment_script.has_value() ? 1 : 0);
84+
if (n_outs < 253)
85+
{
86+
uint8_t cnt = static_cast<uint8_t>(n_outs);
87+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 1));
88+
}
89+
else
90+
{
91+
uint8_t marker = 0xfd;
92+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&marker), 1));
93+
uint16_t cnt = static_cast<uint16_t>(n_outs);
94+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 2));
95+
}
96+
97+
auto write_txout = [&](uint64_t value, const std::vector<unsigned char>& script) {
98+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&value), 8));
99+
BaseScript bs; bs.m_data = script;
100+
tx << bs;
101+
};
102+
103+
// segwit witness-commitment vout (value 0)
104+
if (segwit_commitment_script.has_value())
105+
write_txout(0, segwit_commitment_script.value());
106+
107+
// PPLNS payout outputs (caller supplies final sorted order)
108+
for (auto& [script, amount] : payout_outputs)
109+
write_txout(amount, script);
110+
111+
// donation output
112+
write_txout(donation_amount, donation_script);
113+
114+
// OP_RETURN ref commitment (value 0)
115+
write_txout(0, op_return_script);
116+
117+
// lock_time = 0
118+
{
119+
uint32_t locktime = 0;
120+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&locktime), 4));
121+
}
122+
123+
GentxCoinbase out;
124+
out.bytes.assign(reinterpret_cast<const unsigned char*>(tx.data()),
125+
reinterpret_cast<const unsigned char*>(tx.data()) + tx.size());
126+
auto sp = std::span<const unsigned char>(out.bytes.data(), out.bytes.size());
127+
out.txid = Hash(sp);
128+
return out;
129+
}
130+
131+
} // namespace dgb::coin

src/impl/dgb/test/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ if (BUILD_TESTING AND GTest_FOUND)
2525
# transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear
2626
# in BOTH this ctest registration AND the build.yml --target allowlist,
2727
# or it becomes a #143-style NOT_BUILT sentinel that reds master.
28+
29+
# --- #82 SSOT gentx coinbase assembler KAT ----------------------------
30+
# coin/gentx_coinbase.hpp uses core primitives (PackStream/BaseScript/Hash/
31+
# uint256), so it links the same proven set as dgb_share_test rather than
32+
# the header-only guard foreach above. Oracle ground-truth vectors. MUST
33+
# also be in the build.yml --target allowlist (#143 NOT_BUILT trap).
34+
add_executable(dgb_gentx_coinbase_test gentx_coinbase_test.cpp)
35+
target_link_libraries(dgb_gentx_coinbase_test PRIVATE
36+
GTest::gtest_main GTest::gtest
37+
core dgb
38+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
39+
dgb_coin pool sharechain)
40+
gtest_add_tests(dgb_gentx_coinbase_test "" AUTO)
41+
2842
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test)
2943
add_executable(${dgb_guard} ${dgb_guard}.cpp)
3044
target_link_libraries(${dgb_guard} PRIVATE
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// DGB #82 — SSOT non-witness coinbase (gentx) assembler KAT.
2+
//
3+
// Locks dgb::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp) — the
4+
// single coinbase wire-layout extracted from generate_share_transaction()
5+
// (share_check.hpp:933, the verification SSOT) — against GROUND-TRUTH vectors
6+
// derived from the canonical oracle frstrtr/p2pool-dgb-scrypt
7+
// (util/pack.py byte logic + bitcoin/data.py tx_id_type; donation script
8+
// 4104ffd0...ac). The vectors are NOT self-generated from this builder: they
9+
// are the oracle serializer's output, so a PASS proves emission==oracle, not
10+
// merely self-consistency.
11+
//
12+
// Asserts, for both the no-segwit and witness-commitment-first layouts:
13+
// build -> non-witness serialize == oracle BYTES
14+
// double-SHA256(bytes) (GetHex display) == oracle TXID
15+
// Per-coin isolation: the only DGB<->BCH divergence is the segwit predicate at
16+
// serialize time (witness vout present-or-absent), exercised by both cases.
17+
18+
#include <gtest/gtest.h>
19+
#include <impl/dgb/coin/gentx_coinbase.hpp>
20+
21+
#include <optional>
22+
#include <string>
23+
#include <utility>
24+
#include <vector>
25+
26+
namespace {
27+
28+
std::vector<unsigned char> unhex(const std::string& h) {
29+
std::vector<unsigned char> v; v.reserve(h.size() / 2);
30+
auto nyb = [](char c) -> int { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; };
31+
for (size_t i = 0; i + 1 < h.size(); i += 2)
32+
v.push_back(static_cast<unsigned char>((nyb(h[i]) << 4) | nyb(h[i + 1])));
33+
return v;
34+
}
35+
std::string tohex(const std::vector<unsigned char>& v) {
36+
static const char* H = "0123456789abcdef";
37+
std::string s; s.reserve(v.size() * 2);
38+
for (unsigned char b : v) { s.push_back(H[b >> 4]); s.push_back(H[b & 0xf]); }
39+
return s;
40+
}
41+
42+
// --- fixed inputs shared verbatim with the oracle generator ----------------
43+
const std::vector<unsigned char> CB = unhex("03a1b2c3041122334455667788");
44+
const std::vector<unsigned char> P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac");
45+
const std::vector<unsigned char> P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac");
46+
const std::vector<unsigned char> DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac");
47+
48+
const std::vector<std::pair<std::vector<unsigned char>, uint64_t>> PAYOUTS = {
49+
{P1, 5000000000ull},
50+
{P2, 2500000000ull},
51+
};
52+
53+
// Ground truth from the oracle serializer (see file header).
54+
const std::string NOSEG_BYTES =
55+
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
56+
const std::string NOSEG_TXID =
57+
"c5734775c1521b216e0e1bca506e4d15755cf55125caf56b7e0728a6d54a9b59";
58+
const std::string SEG_BYTES =
59+
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff050000000000000000266a24aa21a9edcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd00f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
60+
const std::string SEG_TXID =
61+
"4b66aabff52ca1336b52688815cece123075856880aaf6e77cb44f0d477b6162";
62+
63+
} // namespace
64+
65+
// op_return / wc literals: build them explicitly to avoid std::string surprises.
66+
static std::vector<unsigned char> make_opret() {
67+
auto v = unhex("6a28");
68+
for (int i = 0; i < 32; ++i) v.push_back(0xab);
69+
const unsigned char nonce[8] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01};
70+
for (unsigned char b : nonce) v.push_back(b);
71+
return v;
72+
}
73+
static std::vector<unsigned char> make_wc() {
74+
auto v = unhex("6a24aa21a9ed");
75+
for (int i = 0; i < 32; ++i) v.push_back(0xcd);
76+
return v;
77+
}
78+
79+
TEST(DGB_gentx_coinbase, NoSegwitOracleVector) {
80+
auto opret = make_opret();
81+
auto g = dgb::coin::assemble_gentx_coinbase(
82+
CB, std::nullopt, PAYOUTS, /*donation_amount=*/1, DON, opret);
83+
EXPECT_EQ(tohex(g.bytes), NOSEG_BYTES);
84+
EXPECT_EQ(g.txid.GetHex(), NOSEG_TXID);
85+
}
86+
87+
TEST(DGB_gentx_coinbase, SegwitCommitmentFirstOracleVector) {
88+
auto opret = make_opret();
89+
auto wc = make_wc();
90+
auto g = dgb::coin::assemble_gentx_coinbase(
91+
CB, std::optional<std::vector<unsigned char>>(wc), PAYOUTS,
92+
/*donation_amount=*/1, DON, opret);
93+
EXPECT_EQ(tohex(g.bytes), SEG_BYTES);
94+
EXPECT_EQ(g.txid.GetHex(), SEG_TXID);
95+
}
96+
97+
// Guard the per-coin isolation invariant: toggling only the segwit predicate
98+
// changes vout-count + prepends exactly one 0-value commitment vout, nothing
99+
// else — the divergence is gated, not a forked framer.
100+
TEST(DGB_gentx_coinbase, SegwitPredicateIsTheOnlyDivergence) {
101+
EXPECT_NE(NOSEG_BYTES, SEG_BYTES);
102+
// both share the identical coinbase-script vin prefix and donation/op_return tail
103+
EXPECT_EQ(NOSEG_BYTES.substr(0, 90), SEG_BYTES.substr(0, 90));
104+
}

0 commit comments

Comments
 (0)