Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
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 \
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 \
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 \
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 \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
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 \
Expand Down Expand Up @@ -213,7 +213,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
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 \
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 \
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 \
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 \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
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 \
Expand Down
106 changes: 106 additions & 0 deletions src/impl/dgb/coin/connection_coinbase.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#pragma once
// ============================================================================
// connection_coinbase.hpp — per-connection Stratum coinbase assembler + split.
//
// Builds the gentx (via the gentx_coinbase.hpp SSOT) for the prospective NEXT
// share at a single Stratum connection, then splits it into the Stratum
// coinb1/coinb2 pair around the 8-byte extranonce slot embedded in the
// OP_RETURN ref commitment. The invariant the submit path relies on:
//
// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes
//
// c2pool places the per-miner extranonce in the OP_RETURN's last_txout_nonce
// slot (NOT the scriptSig), so the split point is purely positional: the
// 8-byte nonce occupies bytes [N-12, N-4) and the 4-byte locktime is [N-4, N).
// This mirrors frstrtr/p2pool-dgb-scrypt work.py get_work() coinbase framing.
//
// Pure: takes an already-resolved scriptSig, the PPLNS payout outputs, the
// donation pair, and the (ref_hash, last_txout_nonce) computed by the caller's
// ShareTracker seam. No tracker / chain dependency, so it is directly KAT-able
// against the canonical oracle wire (see test/connection_coinbase_test.cpp).
// ============================================================================

#include "gentx_coinbase.hpp"

#include <core/uint256.hpp>
#include <util/strencodings.h> // HexStr
#include <btclibs/span.h> // Span

#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace dgb::coin
{

// Caller-resolved inputs for one connection's coinbase. payout_outputs are the
// PPLNS (scriptPubKey, value) pairs already in final consensus order (sorted
// asc by amount, asc by script — see share_check.hpp generate_share_transaction
// step 3). segwit_commitment_script present iff this share carries segwit data.
struct ConnCoinbaseInputs
{
std::vector<unsigned char> coinbase_script; // scriptSig (BIP34 height + pool tag)
std::optional<std::vector<unsigned char>> segwit_commitment_script;
std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs;
uint64_t donation_amount{0};
std::vector<unsigned char> donation_script;
uint256 ref_hash; // p2pool ref_hash (32B)
uint64_t last_txout_nonce{0}; // OP_RETURN nonce (extranonce slot)
};

struct ConnCoinbaseParts
{
std::string coinb1; // hex: gentx bytes up to (not incl.) the 8B nonce slot
std::string coinb2; // hex: locktime tail (the 4 bytes after the nonce slot)
GentxCoinbase gentx; // full non-witness bytes + txid (nonce slot filled in)
};

// OP_RETURN ref commitment script: 0x6a 0x28 || ref_hash(32 LE) || nonce(8 LE).
// Byte-identical to share_check.hpp's commitment (the verification SSOT).
inline std::vector<unsigned char> build_ref_op_return(const uint256& ref_hash,
uint64_t last_txout_nonce)
{
std::vector<unsigned char> s;
s.reserve(2 + 32 + 8);
s.push_back(0x6a); // OP_RETURN
s.push_back(0x28); // PUSH 40 (32 ref_hash + 8 nonce)
auto rb = ref_hash.GetChars(); // 32 LE bytes
s.insert(s.end(), rb.begin(), rb.end());
for (int i = 0; i < 8; ++i)
s.push_back(static_cast<unsigned char>((last_txout_nonce >> (8 * i)) & 0xff));
return s;
}

// Assemble the per-connection coinbase and split it for Stratum.
inline ConnCoinbaseParts build_connection_coinbase_parts(const ConnCoinbaseInputs& in)
{
auto op_return = build_ref_op_return(in.ref_hash, in.last_txout_nonce);

GentxCoinbase g = assemble_gentx_coinbase(
in.coinbase_script,
in.segwit_commitment_script,
in.payout_outputs,
in.donation_amount,
in.donation_script,
op_return);

// Positional split: OP_RETURN is the final vout, so its 8-byte
// last_txout_nonce is at [N-12, N-4) and the locktime at [N-4, N).
// coinb1 = bytes[0, N-12) (up to & incl. ref_hash)
// coinb2 = bytes[N-4, N) (locktime)
// The dropped 8 bytes are filled at submit by extranonce1 || extranonce2.
const auto& b = g.bytes;
const size_t n = b.size();
// n is always >= 12 (vin + >=1 vout + op_return(51) + locktime(4)); the
// op_return alone is 51 bytes, so this holds for every valid assembly.

ConnCoinbaseParts out;
out.coinb1 = HexStr(Span<const unsigned char>(b.data(), n - 12));
out.coinb2 = HexStr(Span<const unsigned char>(b.data() + (n - 4), 4));
out.gentx = std::move(g);
return out;
}

} // namespace dgb::coin
16 changes: 16 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ if (BUILD_TESTING AND GTest_FOUND)
dgb_coin pool sharechain)
gtest_add_tests(dgb_gentx_coinbase_test "" AUTO)

# --- Phase B: per-connection Stratum coinbase assembler + split KAT -----
# Pins coin/connection_coinbase.hpp: build_connection_coinbase_parts builds
# the per-connection gentx via the assemble_gentx_coinbase SSOT and splits
# it into coinb1/coinb2 around the OP_RETURN 8-byte extranonce slot. Proves
# bytes==oracle (same NOSEG vector as dgb_gentx_coinbase_test) AND the
# split invariant coinb1||en1||en2||coinb2 == full gentx. Links the dgb
# OBJECT-lib SCC set like dgb_gentx_coinbase_test. MUST appear in BOTH this
# registration AND the build.yml --target allowlist (#143 NOT_BUILT trap).
add_executable(dgb_connection_coinbase_test connection_coinbase_test.cpp)
target_link_libraries(dgb_connection_coinbase_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_connection_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
Expand Down
104 changes: 104 additions & 0 deletions src/impl/dgb/test/connection_coinbase_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// DGB Phase B — per-connection Stratum coinbase assembler + split KAT.
//
// Locks dgb::coin::build_connection_coinbase_parts() (coin/connection_coinbase.hpp)
// against the SAME ground-truth oracle vector used by the gentx assembler KAT
// (gentx_coinbase_test.cpp NOSEG_BYTES — derived from frstrtr/p2pool-dgb-scrypt
// util/pack.py + donation 4104ffd0...ac). A PASS proves:
//
// 1. the per-connection coinbase serializes byte-identical to the oracle wire
// (the assembler path used == the verification SSOT), AND
// 2. the Stratum split invariant holds:
// coinb1 || extranonce1(4) || extranonce2(4) || coinb2 == full gentx bytes
// with the 8-byte extranonce slot landing exactly on the OP_RETURN
// last_txout_nonce (the submit path reconstructs the header off this).
//
// Pure / no tracker: the (ref_hash, last_txout_nonce) are fixed inputs, so the
// vector is the oracle serializer's output — not self-generated.

#include <gtest/gtest.h>
#include <impl/dgb/coin/connection_coinbase.hpp>

#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace {

std::vector<unsigned char> unhex(const std::string& h) {
std::vector<unsigned char> v; v.reserve(h.size() / 2);
auto nyb = [](char c) -> int { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; };
for (size_t i = 0; i + 1 < h.size(); i += 2)
v.push_back(static_cast<unsigned char>((nyb(h[i]) << 4) | nyb(h[i + 1])));
return v;
}
std::string tohex(const std::vector<unsigned char>& v) {
static const char* H = "0123456789abcdef";
std::string s; s.reserve(v.size() * 2);
for (unsigned char b : v) { s.push_back(H[b >> 4]); s.push_back(H[b & 0xf]); }
return s;
}

// --- inputs shared verbatim with gentx_coinbase_test.cpp NOSEG vector --------
const std::vector<unsigned char> CB = unhex("03a1b2c3041122334455667788");
const std::vector<unsigned char> P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac");
const std::vector<unsigned char> P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac");
const std::vector<unsigned char> DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac");

const std::vector<std::pair<std::vector<unsigned char>, uint64_t>> PAYOUTS = {
{P1, 5000000000ull},
{P2, 2500000000ull},
};

// ref_hash = 0xab * 32 ; last_txout_nonce LE bytes = 08 07 06 05 04 03 02 01.
const uint64_t NONCE = 0x0102030405060708ull;

// Oracle ground truth (identical to gentx_coinbase_test.cpp NOSEG_BYTES).
const std::string NOSEG_BYTES =
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";

dgb::coin::ConnCoinbaseParts build() {
dgb::coin::ConnCoinbaseInputs in;
in.coinbase_script = CB;
in.segwit_commitment_script = std::nullopt;
in.payout_outputs = PAYOUTS;
in.donation_amount = 1; // matches NOSEG donation value (1 sat)
in.donation_script = DON;
in.ref_hash = uint256(std::vector<unsigned char>(32, 0xab));
in.last_txout_nonce = NONCE;
return dgb::coin::build_connection_coinbase_parts(in);
}

// (1) full assembled bytes == oracle wire.
TEST(ConnCoinbase, FullBytesMatchOracle) {
auto parts = build();
EXPECT_EQ(tohex(parts.gentx.bytes), NOSEG_BYTES);
}

// (2) the Stratum split reassembles to the exact oracle bytes with the
// 8-byte extranonce slot filling the OP_RETURN nonce.
TEST(ConnCoinbase, SplitReassemblesToOracle) {
auto parts = build();
const std::string extranonce = "0807060504030201"; // en1(4)||en2(4), LE nonce
EXPECT_EQ(parts.coinb1 + extranonce + parts.coinb2, NOSEG_BYTES);
}

// (3) coinb1 is the prefix up to (excl.) the nonce slot; coinb2 is the locktime.
TEST(ConnCoinbase, Coinb1PrefixCoinb2Locktime) {
auto parts = build();
ASSERT_GE(NOSEG_BYTES.size(), size_t{24});
// coinb1 == oracle minus the last 12 bytes (nonce 8 + locktime 4 = 24 hex)
EXPECT_EQ(parts.coinb1, NOSEG_BYTES.substr(0, NOSEG_BYTES.size() - 24));
// coinb2 == final 4 bytes (locktime), all-zero
EXPECT_EQ(parts.coinb2, "00000000");
}

// (4) the OP_RETURN ref script is the canonical 6a28 || ref_hash || nonce(LE).
TEST(ConnCoinbase, RefOpReturnLayout) {
auto op = dgb::coin::build_ref_op_return(
uint256(std::vector<unsigned char>(32, 0xab)), NONCE);
std::string ref32; for (int i = 0; i < 32; ++i) ref32 += "ab";
EXPECT_EQ(tohex(op), std::string("6a28") + ref32 + "0807060504030201");
}

} // namespace
Loading