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
132 changes: 132 additions & 0 deletions src/impl/btc/coin/gentx_coinbase.hpp
Original file line number Diff line number Diff line change
@@ -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 <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 btc::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 btc::coin
2 changes: 1 addition & 1 deletion src/impl/btc/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
105 changes: 105 additions & 0 deletions src/impl/btc/test/gentx_coinbase_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>
#include <impl/btc/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(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<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(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));
}
Loading