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 @@ -67,7 +67,7 @@ jobs:
test_phase4_embedded \
test_mweb_builder \
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
v37_test \
-j$(nproc)
Expand Down Expand Up @@ -197,7 +197,7 @@ jobs:
test_phase4_embedded \
test_mweb_builder \
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
Expand Down
131 changes: 131 additions & 0 deletions src/impl/dgb/coin/gentx_coinbase.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#pragma once
// ============================================================================
// gentx_coinbase.hpp — SSOT non-witness coinbase (gentx) assembler.
//
// Single source of the p2pool coinbase wire layout. Consumed by:
// - share_check.hpp generate_share_transaction() (the verification SSOT)
// - share_check.hpp create_local_share() (the "same format as" smell)
// - won-block reconstruction (as_block framing)
// so that emission and verification can never diverge on a byte.
//
// Produces the NON-WITNESS serialization and its double-SHA256 txid
// (== p2pool gentx_hash). Byte layout mirrors
// frstrtr/p2pool-merged-v36 data.py generate_transaction():
//
// version(4 LE = 1)
// vin_count(varint = 1)
// vin[0] = prev_hash(32 zero) | prev_idx(0xffffffff) | script(VarStr) | seq(0xffffffff)
// vout_count(varint)
// vouts: [segwit_commitment?] ++ payout_outputs ++ donation ++ op_return_commitment
// each vout = value(8 LE) | script(VarStr)
// lock_time(4 = 0)
//
// Pure: takes already-built script/amount inputs (no tracker, no share template
// dependency) so it is directly KAT-able against a canonical oracle vector.
// ============================================================================

#include <core/pack.hpp>
#include <core/pack_types.hpp>
#include <core/hash.hpp>
#include <core/uint256.hpp>

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

namespace dgb::coin
{

struct GentxCoinbase
{
std::vector<unsigned char> bytes; // non-witness serialization
uint256 txid; // double-SHA256(bytes) == gentx_hash
};

// payout_outputs: (scriptPubKey, value) pairs in final consensus order.
// segwit_commitment_script / segwit absent -> no witness-commitment vout.
inline GentxCoinbase assemble_gentx_coinbase(
const std::vector<unsigned char>& coinbase_script,
const std::optional<std::vector<unsigned char>>& segwit_commitment_script,
const std::vector<std::pair<std::vector<unsigned char>, uint64_t>>& payout_outputs,
uint64_t donation_amount,
const std::vector<unsigned char>& donation_script,
const std::vector<unsigned char>& op_return_script)
{
PackStream tx;

// tx version = 1
uint32_t tx_version = 1;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&tx_version), 4));

// vin count = 1
{
unsigned char one = 1;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&one), 1));
}

// vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0xffffffff
{
uint256 zero_hash;
tx << zero_hash;
uint32_t prev_idx = 0xffffffff;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&prev_idx), 4));
BaseScript cb; cb.m_data = coinbase_script;
tx << cb;
uint32_t seq = 0xffffffff;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&seq), 4));
}

// vout count
size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN */
+ (segwit_commitment_script.has_value() ? 1 : 0);
if (n_outs < 253)
{
uint8_t cnt = static_cast<uint8_t>(n_outs);
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 1));
}
else
{
uint8_t marker = 0xfd;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&marker), 1));
uint16_t cnt = static_cast<uint16_t>(n_outs);
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 2));
}

auto write_txout = [&](uint64_t value, const std::vector<unsigned char>& script) {
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&value), 8));
BaseScript bs; bs.m_data = script;
tx << bs;
};

// segwit witness-commitment vout (value 0)
if (segwit_commitment_script.has_value())
write_txout(0, segwit_commitment_script.value());

// PPLNS payout outputs (caller supplies final sorted order)
for (auto& [script, amount] : payout_outputs)
write_txout(amount, script);

// donation output
write_txout(donation_amount, donation_script);

// OP_RETURN ref commitment (value 0)
write_txout(0, op_return_script);

// lock_time = 0
{
uint32_t locktime = 0;
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&locktime), 4));
}

GentxCoinbase out;
out.bytes.assign(reinterpret_cast<const unsigned char*>(tx.data()),
reinterpret_cast<const unsigned char*>(tx.data()) + tx.size());
auto sp = std::span<const unsigned char>(out.bytes.data(), out.bytes.size());
out.txid = Hash(sp);
return out;
}

} // namespace dgb::coin
14 changes: 14 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ if (BUILD_TESTING AND GTest_FOUND)
# transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear
# in BOTH this ctest registration AND the build.yml --target allowlist,
# or it becomes a #143-style NOT_BUILT sentinel that reds master.

# --- #82 SSOT gentx coinbase assembler KAT ----------------------------
# coin/gentx_coinbase.hpp uses core primitives (PackStream/BaseScript/Hash/
# uint256), so it links the same proven set as dgb_share_test rather than
# the header-only guard foreach above. Oracle ground-truth vectors. MUST
# also be in the build.yml --target allowlist (#143 NOT_BUILT trap).
add_executable(dgb_gentx_coinbase_test gentx_coinbase_test.cpp)
target_link_libraries(dgb_gentx_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_gentx_coinbase_test "" AUTO)

foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test)
add_executable(${dgb_guard} ${dgb_guard}.cpp)
target_link_libraries(${dgb_guard} PRIVATE
Expand Down
104 changes: 104 additions & 0 deletions src/impl/dgb/test/gentx_coinbase_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// DGB #82 — SSOT non-witness coinbase (gentx) assembler KAT.
//
// Locks dgb::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp) — the
// single coinbase wire-layout extracted from generate_share_transaction()
// (share_check.hpp:933, the verification SSOT) — against GROUND-TRUTH vectors
// derived from the canonical oracle frstrtr/p2pool-dgb-scrypt
// (util/pack.py byte logic + bitcoin/data.py tx_id_type; donation script
// 4104ffd0...ac). The vectors are NOT self-generated from this builder: they
// are the oracle serializer's output, so a PASS proves emission==oracle, not
// merely self-consistency.
//
// Asserts, for both the no-segwit and witness-commitment-first layouts:
// build -> non-witness serialize == oracle BYTES
// double-SHA256(bytes) (GetHex display) == oracle TXID
// Per-coin isolation: the only DGB<->BCH divergence is the segwit predicate at
// serialize time (witness vout present-or-absent), exercised by both cases.

#include <gtest/gtest.h>
#include <impl/dgb/coin/gentx_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;
}

// --- fixed inputs shared verbatim with the oracle generator ----------------
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},
};

// Ground truth from the oracle serializer (see file header).
const std::string NOSEG_BYTES =
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
const std::string NOSEG_TXID =
"c5734775c1521b216e0e1bca506e4d15755cf55125caf56b7e0728a6d54a9b59";
const std::string SEG_BYTES =
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff050000000000000000266a24aa21a9edcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd00f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
const std::string SEG_TXID =
"4b66aabff52ca1336b52688815cece123075856880aaf6e77cb44f0d477b6162";

} // namespace

// op_return / wc literals: build them explicitly to avoid std::string surprises.
static std::vector<unsigned char> make_opret() {
auto v = unhex("6a28");
for (int i = 0; i < 32; ++i) v.push_back(0xab);
const unsigned char nonce[8] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01};
for (unsigned char b : nonce) v.push_back(b);
return v;
}
static std::vector<unsigned char> make_wc() {
auto v = unhex("6a24aa21a9ed");
for (int i = 0; i < 32; ++i) v.push_back(0xcd);
return v;
}

TEST(DGB_gentx_coinbase, NoSegwitOracleVector) {
auto opret = make_opret();
auto g = dgb::coin::assemble_gentx_coinbase(
CB, std::nullopt, PAYOUTS, /*donation_amount=*/1, DON, opret);
EXPECT_EQ(tohex(g.bytes), NOSEG_BYTES);
EXPECT_EQ(g.txid.GetHex(), NOSEG_TXID);
}

TEST(DGB_gentx_coinbase, SegwitCommitmentFirstOracleVector) {
auto opret = make_opret();
auto wc = make_wc();
auto g = dgb::coin::assemble_gentx_coinbase(
CB, std::optional<std::vector<unsigned char>>(wc), PAYOUTS,
/*donation_amount=*/1, DON, opret);
EXPECT_EQ(tohex(g.bytes), SEG_BYTES);
EXPECT_EQ(g.txid.GetHex(), SEG_TXID);
}

// Guard the per-coin isolation invariant: toggling only the segwit predicate
// changes vout-count + prepends exactly one 0-value commitment vout, nothing
// else — the divergence is gated, not a forked framer.
TEST(DGB_gentx_coinbase, SegwitPredicateIsTheOnlyDivergence) {
EXPECT_NE(NOSEG_BYTES, SEG_BYTES);
// both share the identical coinbase-script vin prefix and donation/op_return tail
EXPECT_EQ(NOSEG_BYTES.substr(0, 90), SEG_BYTES.substr(0, 90));
}
Loading