diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 887264d1d..f2a5571c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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) @@ -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 \ diff --git a/src/impl/dgb/coin/gentx_coinbase.hpp b/src/impl/dgb/coin/gentx_coinbase.hpp new file mode 100644 index 000000000..20a15d466 --- /dev/null +++ b/src/impl/dgb/coin/gentx_coinbase.hpp @@ -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 +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace dgb::coin +{ + +struct GentxCoinbase +{ + std::vector 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& coinbase_script, + const std::optional>& segwit_commitment_script, + const std::vector, uint64_t>>& payout_outputs, + uint64_t donation_amount, + const std::vector& donation_script, + const std::vector& op_return_script) +{ + PackStream tx; + + // tx version = 1 + uint32_t tx_version = 1; + tx.write(std::span(reinterpret_cast(&tx_version), 4)); + + // vin count = 1 + { + unsigned char one = 1; + tx.write(std::span(reinterpret_cast(&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(reinterpret_cast(&prev_idx), 4)); + BaseScript cb; cb.m_data = coinbase_script; + tx << cb; + uint32_t seq = 0xffffffff; + tx.write(std::span(reinterpret_cast(&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(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 1)); + } + else + { + uint8_t marker = 0xfd; + tx.write(std::span(reinterpret_cast(&marker), 1)); + uint16_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 2)); + } + + auto write_txout = [&](uint64_t value, const std::vector& script) { + tx.write(std::span(reinterpret_cast(&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(reinterpret_cast(&locktime), 4)); + } + + GentxCoinbase out; + out.bytes.assign(reinterpret_cast(tx.data()), + reinterpret_cast(tx.data()) + tx.size()); + auto sp = std::span(out.bytes.data(), out.bytes.size()); + out.txid = Hash(sp); + return out; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 683aeb525..26ee111be 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -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 diff --git a/src/impl/dgb/test/gentx_coinbase_test.cpp b/src/impl/dgb/test/gentx_coinbase_test.cpp new file mode 100644 index 000000000..e96b47caf --- /dev/null +++ b/src/impl/dgb/test/gentx_coinbase_test.cpp @@ -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 +#include + +#include +#include +#include +#include + +namespace { + +std::vector unhex(const std::string& h) { + std::vector 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((nyb(h[i]) << 4) | nyb(h[i + 1]))); + return v; +} +std::string tohex(const std::vector& 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 CB = unhex("03a1b2c3041122334455667788"); +const std::vector P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac"); +const std::vector P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac"); +const std::vector DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac"); + +const std::vector, 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 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 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>(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)); +}