Skip to content

Commit 09d2d7f

Browse files
authored
Merge pull request #327 from frstrtr/dgb/conn-coinbase-assembler-split
dgb: per-connection Stratum coinbase assembler + coinb1/coinb2 split SSOT
2 parents d5ddb71 + 1f4f800 commit 09d2d7f

4 files changed

Lines changed: 228 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
test_mweb_builder \
8282
test_address_resolution test_compute_share_target \
8383
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 \
84-
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 \
84+
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 \
8585
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 \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
8787
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:
213213
test_mweb_builder \
214214
test_address_resolution test_compute_share_target \
215215
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 \
216-
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 \
216+
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 \
217217
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 \
218218
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219219
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 \
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#pragma once
2+
// ============================================================================
3+
// connection_coinbase.hpp — per-connection Stratum coinbase assembler + split.
4+
//
5+
// Builds the gentx (via the gentx_coinbase.hpp SSOT) for the prospective NEXT
6+
// share at a single Stratum connection, then splits it into the Stratum
7+
// coinb1/coinb2 pair around the 8-byte extranonce slot embedded in the
8+
// OP_RETURN ref commitment. The invariant the submit path relies on:
9+
//
10+
// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes
11+
//
12+
// c2pool places the per-miner extranonce in the OP_RETURN's last_txout_nonce
13+
// slot (NOT the scriptSig), so the split point is purely positional: the
14+
// 8-byte nonce occupies bytes [N-12, N-4) and the 4-byte locktime is [N-4, N).
15+
// This mirrors frstrtr/p2pool-dgb-scrypt work.py get_work() coinbase framing.
16+
//
17+
// Pure: takes an already-resolved scriptSig, the PPLNS payout outputs, the
18+
// donation pair, and the (ref_hash, last_txout_nonce) computed by the caller's
19+
// ShareTracker seam. No tracker / chain dependency, so it is directly KAT-able
20+
// against the canonical oracle wire (see test/connection_coinbase_test.cpp).
21+
// ============================================================================
22+
23+
#include "gentx_coinbase.hpp"
24+
25+
#include <core/uint256.hpp>
26+
#include <util/strencodings.h> // HexStr
27+
#include <btclibs/span.h> // Span
28+
29+
#include <cstdint>
30+
#include <optional>
31+
#include <string>
32+
#include <utility>
33+
#include <vector>
34+
35+
namespace dgb::coin
36+
{
37+
38+
// Caller-resolved inputs for one connection's coinbase. payout_outputs are the
39+
// PPLNS (scriptPubKey, value) pairs already in final consensus order (sorted
40+
// asc by amount, asc by script — see share_check.hpp generate_share_transaction
41+
// step 3). segwit_commitment_script present iff this share carries segwit data.
42+
struct ConnCoinbaseInputs
43+
{
44+
std::vector<unsigned char> coinbase_script; // scriptSig (BIP34 height + pool tag)
45+
std::optional<std::vector<unsigned char>> segwit_commitment_script;
46+
std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs;
47+
uint64_t donation_amount{0};
48+
std::vector<unsigned char> donation_script;
49+
uint256 ref_hash; // p2pool ref_hash (32B)
50+
uint64_t last_txout_nonce{0}; // OP_RETURN nonce (extranonce slot)
51+
};
52+
53+
struct ConnCoinbaseParts
54+
{
55+
std::string coinb1; // hex: gentx bytes up to (not incl.) the 8B nonce slot
56+
std::string coinb2; // hex: locktime tail (the 4 bytes after the nonce slot)
57+
GentxCoinbase gentx; // full non-witness bytes + txid (nonce slot filled in)
58+
};
59+
60+
// OP_RETURN ref commitment script: 0x6a 0x28 || ref_hash(32 LE) || nonce(8 LE).
61+
// Byte-identical to share_check.hpp's commitment (the verification SSOT).
62+
inline std::vector<unsigned char> build_ref_op_return(const uint256& ref_hash,
63+
uint64_t last_txout_nonce)
64+
{
65+
std::vector<unsigned char> s;
66+
s.reserve(2 + 32 + 8);
67+
s.push_back(0x6a); // OP_RETURN
68+
s.push_back(0x28); // PUSH 40 (32 ref_hash + 8 nonce)
69+
auto rb = ref_hash.GetChars(); // 32 LE bytes
70+
s.insert(s.end(), rb.begin(), rb.end());
71+
for (int i = 0; i < 8; ++i)
72+
s.push_back(static_cast<unsigned char>((last_txout_nonce >> (8 * i)) & 0xff));
73+
return s;
74+
}
75+
76+
// Assemble the per-connection coinbase and split it for Stratum.
77+
inline ConnCoinbaseParts build_connection_coinbase_parts(const ConnCoinbaseInputs& in)
78+
{
79+
auto op_return = build_ref_op_return(in.ref_hash, in.last_txout_nonce);
80+
81+
GentxCoinbase g = assemble_gentx_coinbase(
82+
in.coinbase_script,
83+
in.segwit_commitment_script,
84+
in.payout_outputs,
85+
in.donation_amount,
86+
in.donation_script,
87+
op_return);
88+
89+
// Positional split: OP_RETURN is the final vout, so its 8-byte
90+
// last_txout_nonce is at [N-12, N-4) and the locktime at [N-4, N).
91+
// coinb1 = bytes[0, N-12) (up to & incl. ref_hash)
92+
// coinb2 = bytes[N-4, N) (locktime)
93+
// The dropped 8 bytes are filled at submit by extranonce1 || extranonce2.
94+
const auto& b = g.bytes;
95+
const size_t n = b.size();
96+
// n is always >= 12 (vin + >=1 vout + op_return(51) + locktime(4)); the
97+
// op_return alone is 51 bytes, so this holds for every valid assembly.
98+
99+
ConnCoinbaseParts out;
100+
out.coinb1 = HexStr(Span<const unsigned char>(b.data(), n - 12));
101+
out.coinb2 = HexStr(Span<const unsigned char>(b.data() + (n - 4), 4));
102+
out.gentx = std::move(g);
103+
return out;
104+
}
105+
106+
} // namespace dgb::coin

src/impl/dgb/test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ if (BUILD_TESTING AND GTest_FOUND)
145145
dgb_coin pool sharechain)
146146
gtest_add_tests(dgb_gentx_coinbase_test "" AUTO)
147147

148+
# --- Phase B: per-connection Stratum coinbase assembler + split KAT -----
149+
# Pins coin/connection_coinbase.hpp: build_connection_coinbase_parts builds
150+
# the per-connection gentx via the assemble_gentx_coinbase SSOT and splits
151+
# it into coinb1/coinb2 around the OP_RETURN 8-byte extranonce slot. Proves
152+
# bytes==oracle (same NOSEG vector as dgb_gentx_coinbase_test) AND the
153+
# split invariant coinb1||en1||en2||coinb2 == full gentx. Links the dgb
154+
# OBJECT-lib SCC set like dgb_gentx_coinbase_test. MUST appear in BOTH this
155+
# registration AND the build.yml --target allowlist (#143 NOT_BUILT trap).
156+
add_executable(dgb_connection_coinbase_test connection_coinbase_test.cpp)
157+
target_link_libraries(dgb_connection_coinbase_test PRIVATE
158+
GTest::gtest_main GTest::gtest
159+
core dgb
160+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
161+
dgb_coin pool sharechain)
162+
gtest_add_tests(dgb_connection_coinbase_test "" AUTO)
163+
148164
# --- #172 share-path gentx KAT (collapse consumption proof) ------------
149165
# Proves generate_share_transaction (the verification SSOT call site #172
150166
# rewired) EMITS assemble_gentx_coinbase framing for a real v36 share, via
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// DGB Phase B — per-connection Stratum coinbase assembler + split KAT.
2+
//
3+
// Locks dgb::coin::build_connection_coinbase_parts() (coin/connection_coinbase.hpp)
4+
// against the SAME ground-truth oracle vector used by the gentx assembler KAT
5+
// (gentx_coinbase_test.cpp NOSEG_BYTES — derived from frstrtr/p2pool-dgb-scrypt
6+
// util/pack.py + donation 4104ffd0...ac). A PASS proves:
7+
//
8+
// 1. the per-connection coinbase serializes byte-identical to the oracle wire
9+
// (the assembler path used == the verification SSOT), AND
10+
// 2. the Stratum split invariant holds:
11+
// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes
12+
// with the 8-byte extranonce slot landing exactly on the OP_RETURN
13+
// last_txout_nonce (the submit path reconstructs the header off this).
14+
//
15+
// Pure / no tracker: the (ref_hash, last_txout_nonce) are fixed inputs, so the
16+
// vector is the oracle serializer's output — not self-generated.
17+
18+
#include <gtest/gtest.h>
19+
#include <impl/dgb/coin/connection_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+
// --- inputs shared verbatim with gentx_coinbase_test.cpp NOSEG vector --------
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+
// ref_hash = 0xab * 32 ; last_txout_nonce LE bytes = 08 07 06 05 04 03 02 01.
54+
const uint64_t NONCE = 0x0102030405060708ull;
55+
56+
// Oracle ground truth (identical to gentx_coinbase_test.cpp NOSEG_BYTES).
57+
const std::string NOSEG_BYTES =
58+
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
59+
60+
dgb::coin::ConnCoinbaseParts build() {
61+
dgb::coin::ConnCoinbaseInputs in;
62+
in.coinbase_script = CB;
63+
in.segwit_commitment_script = std::nullopt;
64+
in.payout_outputs = PAYOUTS;
65+
in.donation_amount = 1; // matches NOSEG donation value (1 sat)
66+
in.donation_script = DON;
67+
in.ref_hash = uint256(std::vector<unsigned char>(32, 0xab));
68+
in.last_txout_nonce = NONCE;
69+
return dgb::coin::build_connection_coinbase_parts(in);
70+
}
71+
72+
// (1) full assembled bytes == oracle wire.
73+
TEST(ConnCoinbase, FullBytesMatchOracle) {
74+
auto parts = build();
75+
EXPECT_EQ(tohex(parts.gentx.bytes), NOSEG_BYTES);
76+
}
77+
78+
// (2) the Stratum split reassembles to the exact oracle bytes with the
79+
// 8-byte extranonce slot filling the OP_RETURN nonce.
80+
TEST(ConnCoinbase, SplitReassemblesToOracle) {
81+
auto parts = build();
82+
const std::string extranonce = "0807060504030201"; // en1(4)||en2(4), LE nonce
83+
EXPECT_EQ(parts.coinb1 + extranonce + parts.coinb2, NOSEG_BYTES);
84+
}
85+
86+
// (3) coinb1 is the prefix up to (excl.) the nonce slot; coinb2 is the locktime.
87+
TEST(ConnCoinbase, Coinb1PrefixCoinb2Locktime) {
88+
auto parts = build();
89+
ASSERT_GE(NOSEG_BYTES.size(), size_t{24});
90+
// coinb1 == oracle minus the last 12 bytes (nonce 8 + locktime 4 = 24 hex)
91+
EXPECT_EQ(parts.coinb1, NOSEG_BYTES.substr(0, NOSEG_BYTES.size() - 24));
92+
// coinb2 == final 4 bytes (locktime), all-zero
93+
EXPECT_EQ(parts.coinb2, "00000000");
94+
}
95+
96+
// (4) the OP_RETURN ref script is the canonical 6a28 || ref_hash || nonce(LE).
97+
TEST(ConnCoinbase, RefOpReturnLayout) {
98+
auto op = dgb::coin::build_ref_op_return(
99+
uint256(std::vector<unsigned char>(32, 0xab)), NONCE);
100+
std::string ref32; for (int i = 0; i < 32; ++i) ref32 += "ab";
101+
EXPECT_EQ(tohex(op), std::string("6a28") + ref32 + "0807060504030201");
102+
}
103+
104+
} // namespace

0 commit comments

Comments
 (0)