diff --git a/src/impl/btc/coin/gentx_unpack.hpp b/src/impl/btc/coin/gentx_unpack.hpp new file mode 100644 index 000000000..8bf8efa8c --- /dev/null +++ b/src/impl/btc/coin/gentx_unpack.hpp @@ -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 +#include +#include + +#include +#include +#include + +#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& 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 diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index e675af715..b56634d39 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) + 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 diff --git a/src/impl/btc/test/gentx_unpack_test.cpp b/src/impl/btc/test/gentx_unpack_test.cpp new file mode 100644 index 000000000..6357ad5d2 --- /dev/null +++ b/src/impl/btc/test/gentx_unpack_test.cpp @@ -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 +#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; +} + +// 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 v( + reinterpret_cast(sp.data()), + reinterpret_cast(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); +}