diff --git a/src/impl/btc/coin/gentx_coinbase.hpp b/src/impl/btc/coin/gentx_coinbase.hpp new file mode 100644 index 000000000..69953b7bf --- /dev/null +++ b/src/impl/btc/coin/gentx_coinbase.hpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#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 (BTC parent) 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 btc::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 btc::coin \ No newline at end of file diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index b56634d39..1ac6eb6d1 100644 --- a/src/impl/btc/test/CMakeLists.txt +++ b/src/impl/btc/test/CMakeLists.txt @@ -1,7 +1,7 @@ if (BUILD_TESTING AND GTest_FOUND) # btc twin of ltc share_test — uniquely named to avoid the CMP0002 target # collision with src/impl/ltc/test (both subdirs build in the same tree). - add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp) + add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp gentx_coinbase_test.cpp) target_link_libraries(btc_share_test PRIVATE GTest::gtest_main GTest::gtest core btc diff --git a/src/impl/btc/test/gentx_coinbase_test.cpp b/src/impl/btc/test/gentx_coinbase_test.cpp new file mode 100644 index 000000000..6d60c7410 --- /dev/null +++ b/src/impl/btc/test/gentx_coinbase_test.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// BTC (#744 reconstructor slice 2/7) — SSOT non-witness coinbase (gentx) assembler KAT. +// +// Locks btc::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp) — the +// single coinbase wire-layout extracted from generate_share_transaction() +// (share_check.hpp:965, 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 BTC<->DGB 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(BTC_gentx_coinbase, NoSegwitOracleVector) { + auto opret = make_opret(); + auto g = btc::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(BTC_gentx_coinbase, SegwitCommitmentFirstOracleVector) { + auto opret = make_opret(); + auto wc = make_wc(); + auto g = btc::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(BTC_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)); +} \ No newline at end of file