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
98 changes: 98 additions & 0 deletions src/impl/btc/coin/gentx_unpack.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once
// ---------------------------------------------------------------------------
// btc::coin::unpack_gentx_coinbase -- the codec that turns a share's SSOT
// non-witness gentx (coinbase) bytes back into a deserialized MutableTransaction
// ready to inject at block tx index 0 of the won-block reconstructor. It is the
// first landed sub-slice of the faithful share->block reassembly (p2pool data.py
// Share.as_block) that the #744 dispatch handler (won_block_dispatch.hpp) still
// carries only as an INJECTED std::function stub.
//
// In the run-loop the gentx bytes come from BTC's generate_share_transaction
// SSOT (share_check.hpp) -- the single coinbase wire layout consumed by both
// emission and verification -- and reconstruct_won_block(_from_template) injects
// the resulting {MutableTransaction, txid} at tx index 0, taking the remaining
// (non-coinbase) txs from the GBT template the miner was handed (NOT the share's
// tx_hash_refs, which v34+ shares do not carry). This slice is deliberately the
// byte<->object codec step ONLY, kept OUT of the pure composition body so the
// as_block ordering + merkle math stay build-verifiable on injected inputs while
// this round-trip is pinned by its own KAT (gentx_unpack_test.cpp).
//
// CRITICAL: the gentx is a NON-WITNESS coinbase and MUST round-trip its
// non-witness bytes EXACTLY -- the txid (double-SHA256 of the non-witness
// serialization == p2pool gentx_hash) is what the merkle_root walk in the
// won-block framing consumes, so any witness/serialization drift here corrupts
// the assembled block's merkle root and the daemon rejects it. We therefore
// unpack with TX_NO_WITNESS: the coinbase vin count is a non-zero 0x01 (never
// the 0x00 segwit dummy), so this parses version|vin|vout|locktime with no
// witness branch and CANNOT consume a witness marker/flag or attach a witness
// stack. The recovered MutableTransaction has empty witness stacks
// (HasWitness()==false), so re-serializing it -- even inside the TX_WITH_WITNESS
// BlockType codec -- emits the identical non-witness bytes and a stable txid.
//
// Failure posture mirrors the DGB sub-slice (src/impl/dgb/coin/gentx_unpack.hpp):
// trailing bytes after a complete tx mean the input is not a clean gentx
// serialization; we throw std::out_of_range rather than silently accept a
// truncated/over-long blob, since a wrong coinbase hashes to the wrong merkle
// root. The reconstruct closure catches this and fails closed (announce + audit,
// RPC submitblock fallback still attempts) rather than broadcasting a bad block.
//
// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE --
// this is the inverse of an already-oracle-pinned serializer; no share format,
// PoW, coinbase commitment, or PPLNS math is touched.
// ---------------------------------------------------------------------------

#include <stdexcept>
#include <utility>
#include <vector>

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

#include "transaction.hpp" // btc::coin::MutableTransaction, TX_NO_WITNESS

namespace btc
{
namespace coin
{

// The deserialized gentx ready for injection at block tx index 0.
// tx : MutableTransaction reconstructed from the non-witness bytes
// txid : double-SHA256(non-witness serialization) == p2pool gentx_hash
struct UnpackedGentx
{
MutableTransaction tx;
uint256 txid;
};

// Non-witness gentx bytes -> {MutableTransaction, txid}. Throws std::out_of_range
// if the input carries trailing bytes past a complete transaction (malformed
// gentx serialization).
inline UnpackedGentx
unpack_gentx_coinbase(const std::vector<unsigned char>& gentx_bytes)
{
PackStream ps(gentx_bytes);

MutableTransaction tx;
// Non-witness parse: the coinbase vin count is 0x01 (never the segwit 0x00
// dummy), so this reads version|vin|vout|locktime with no witness branch --
// guaranteeing the empty-witness, byte-exact round-trip the merkle_root walk
// depends on.
UnserializeTransaction(tx, ps, TX_NO_WITNESS);

if (!ps.empty())
throw std::out_of_range(
"unpack_gentx_coinbase: trailing bytes after gentx -- "
"input is not a clean non-witness coinbase serialization");

UnpackedGentx out;
// txid = SHA256d of the (canonical) non-witness re-serialization; equals the
// gentx_hash the SSOT serializer computed over the same layout.
out.txid = Hash(pack(TX_NO_WITNESS(tx)).get_span());
out.tx = std::move(tx);
return out;
}

} // namespace coin
} // namespace btc
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)
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)
target_link_libraries(btc_share_test PRIVATE
GTest::gtest_main GTest::gtest
core btc
Expand Down
111 changes: 111 additions & 0 deletions src/impl/btc/test/gentx_unpack_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// BTC won-block reconstructor -- gentx-bytes -> MutableTransaction unpack KAT.
//
// Locks btc::coin::unpack_gentx_coinbase() (coin/gentx_unpack.hpp), the codec
// step the run-loop uses to turn a share's SSOT non-witness gentx bytes back
// into the MutableTransaction reconstruct_won_block injects at block tx index 0.
//
// The BTC non-witness coinbase serialization is byte-identical to the generic
// Bitcoin tx codec (version|vin|vout|locktime), so the ground-truth vectors are
// coin-agnostic: each (bytes, txid) pair is a pure double-SHA256 relation over
// the exact bytes, shared verbatim with the DGB sibling KAT
// (src/impl/dgb/test/gentx_unpack_test.cpp) whose CI already pins the pairing.
// This test proves BTC's identical codec recovers the SAME bytes + txid.
//
// Proof legs (all ORACLE-PINNED against fixed vectors -- no self-generation):
// 1. unpack(oracle_bytes) -> re-serialize TX_NO_WITNESS == oracle_bytes
// EXACTLY, and txid == oracle_txid (the byte-exact, txid-stable round-trip
// the merkle_root walk depends on; any drift corrupts the assembled block).
// 2. HasWitness()==false on both layouts (no witness drift; the segwit
// commitment is a vout, not a witness).
// 3. Structural sanity: the recovered coinbase carries the p2pool gentx shape.
// 4. A trailing byte past a complete tx is a malformed gentx -> throw.
//
// The inverse-pairing-vs-SSOT leg (assemble -> unpack) lands with the BTC
// gentx_coinbase.hpp SSOT-exposure slice, which does not yet exist on BTC.
//
// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE.

#include <gtest/gtest.h>
#include <impl/btc/coin/gentx_unpack.hpp>

#include <core/pack.hpp>

#include <stdexcept>
#include <string>
#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;
}

// Re-serialize a MutableTransaction in non-witness form and hex it (the exact
// bytes the txid + merkle_root walk are taken over).
std::string noseg_hex(const btc::coin::MutableTransaction& tx) {
auto packed = pack(btc::coin::TX_NO_WITNESS(tx));
auto sp = packed.get_span();
std::vector<unsigned char> v(
reinterpret_cast<const unsigned char*>(sp.data()),
reinterpret_cast<const unsigned char*>(sp.data()) + sp.size());
return tohex(v);
}

// Ground truth: coin-agnostic non-witness coinbase serializations + their
// SHA256d txids, shared verbatim with the DGB sibling KAT.
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

// (1a) ORACLE round-trip, no-segwit layout: bytes exact + txid stable.
TEST(BTC_gentx_unpack, NoSegwitOracleRoundTrip) {
auto u = btc::coin::unpack_gentx_coinbase(unhex(NOSEG_BYTES));
EXPECT_EQ(noseg_hex(u.tx), NOSEG_BYTES); // byte-exact non-witness re-serialize
EXPECT_EQ(u.txid.GetHex(), NOSEG_TXID); // gentx_hash stable
EXPECT_FALSE(u.tx.HasWitness()); // no witness drift
}

// (1b) ORACLE round-trip, witness-commitment-first layout: still non-witness.
TEST(BTC_gentx_unpack, SegwitCommitmentFirstOracleRoundTrip) {
auto u = btc::coin::unpack_gentx_coinbase(unhex(SEG_BYTES));
EXPECT_EQ(noseg_hex(u.tx), SEG_BYTES);
EXPECT_EQ(u.txid.GetHex(), SEG_TXID);
EXPECT_FALSE(u.tx.HasWitness()); // commitment is a vout, not a witness
}

// (3) Structural sanity: the recovered coinbase has the p2pool gentx shape.
TEST(BTC_gentx_unpack, RecoversCoinbaseShape) {
auto u = btc::coin::unpack_gentx_coinbase(unhex(NOSEG_BYTES));
EXPECT_EQ(u.tx.version, 1);
EXPECT_EQ(u.tx.locktime, 0u);
ASSERT_EQ(u.tx.vin.size(), 1u);
EXPECT_TRUE(u.tx.vin[0].prevout.hash.IsNull()); // coinbase: null prev
EXPECT_EQ(u.tx.vin[0].prevout.index, 0xffffffffu);
EXPECT_EQ(u.tx.vin[0].sequence, 0xffffffffu);
EXPECT_EQ(u.tx.vout.size(), 4u); // P1,P2,donation,op_return
}

// (4) Robustness: a trailing byte past a complete tx is a malformed gentx -> throw.
TEST(BTC_gentx_unpack, TrailingBytesThrow) {
auto bytes = unhex(NOSEG_BYTES);
bytes.push_back(0x00); // one extra byte after a complete tx
EXPECT_THROW(btc::coin::unpack_gentx_coinbase(bytes), std::out_of_range);
}
Loading