Skip to content

Commit 3fc1213

Browse files
authored
Merge pull request #835 from frstrtr/btc/gentx-coinbase-ssot
btc(#744): gentx_coinbase SSOT assembler -- reconstructor slice 2/7
2 parents cb4ba26 + c46d7e7 commit 3fc1213

3 files changed

Lines changed: 238 additions & 1 deletion

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ============================================================================
4+
// gentx_coinbase.hpp — SSOT non-witness coinbase (gentx) assembler.
5+
//
6+
// Single source of the p2pool coinbase wire layout. Consumed by:
7+
// - share_check.hpp generate_share_transaction() (the verification SSOT)
8+
// - share_check.hpp create_local_share() (the "same format as" smell)
9+
// - won-block reconstruction (as_block framing)
10+
// so that emission and verification can never diverge on a byte.
11+
//
12+
// Produces the NON-WITNESS serialization and its double-SHA256 txid
13+
// (== p2pool gentx_hash). Byte layout mirrors
14+
// frstrtr/p2pool-merged-v36 data.py (BTC parent) generate_transaction():
15+
//
16+
// version(4 LE = 1)
17+
// vin_count(varint = 1)
18+
// vin[0] = prev_hash(32 zero) | prev_idx(0xffffffff) | script(VarStr) | seq(0xffffffff)
19+
// vout_count(varint)
20+
// vouts: [segwit_commitment?] ++ payout_outputs ++ donation ++ op_return_commitment
21+
// each vout = value(8 LE) | script(VarStr)
22+
// lock_time(4 = 0)
23+
//
24+
// Pure: takes already-built script/amount inputs (no tracker, no share template
25+
// dependency) so it is directly KAT-able against a canonical oracle vector.
26+
// ============================================================================
27+
28+
#include <core/pack.hpp>
29+
#include <core/pack_types.hpp>
30+
#include <core/hash.hpp>
31+
#include <core/uint256.hpp>
32+
33+
#include <cstdint>
34+
#include <optional>
35+
#include <span>
36+
#include <utility>
37+
#include <vector>
38+
39+
namespace btc::coin
40+
{
41+
42+
struct GentxCoinbase
43+
{
44+
std::vector<unsigned char> bytes; // non-witness serialization
45+
uint256 txid; // double-SHA256(bytes) == gentx_hash
46+
};
47+
48+
// payout_outputs: (scriptPubKey, value) pairs in final consensus order.
49+
// segwit_commitment_script / segwit absent -> no witness-commitment vout.
50+
inline GentxCoinbase assemble_gentx_coinbase(
51+
const std::vector<unsigned char>& coinbase_script,
52+
const std::optional<std::vector<unsigned char>>& segwit_commitment_script,
53+
const std::vector<std::pair<std::vector<unsigned char>, uint64_t>>& payout_outputs,
54+
uint64_t donation_amount,
55+
const std::vector<unsigned char>& donation_script,
56+
const std::vector<unsigned char>& op_return_script)
57+
{
58+
PackStream tx;
59+
60+
// tx version = 1
61+
uint32_t tx_version = 1;
62+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&tx_version), 4));
63+
64+
// vin count = 1
65+
{
66+
unsigned char one = 1;
67+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&one), 1));
68+
}
69+
70+
// vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0xffffffff
71+
{
72+
uint256 zero_hash;
73+
tx << zero_hash;
74+
uint32_t prev_idx = 0xffffffff;
75+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&prev_idx), 4));
76+
BaseScript cb; cb.m_data = coinbase_script;
77+
tx << cb;
78+
uint32_t seq = 0xffffffff;
79+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&seq), 4));
80+
}
81+
82+
// vout count
83+
size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN */
84+
+ (segwit_commitment_script.has_value() ? 1 : 0);
85+
if (n_outs < 253)
86+
{
87+
uint8_t cnt = static_cast<uint8_t>(n_outs);
88+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 1));
89+
}
90+
else
91+
{
92+
uint8_t marker = 0xfd;
93+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&marker), 1));
94+
uint16_t cnt = static_cast<uint16_t>(n_outs);
95+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&cnt), 2));
96+
}
97+
98+
auto write_txout = [&](uint64_t value, const std::vector<unsigned char>& script) {
99+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&value), 8));
100+
BaseScript bs; bs.m_data = script;
101+
tx << bs;
102+
};
103+
104+
// segwit witness-commitment vout (value 0)
105+
if (segwit_commitment_script.has_value())
106+
write_txout(0, segwit_commitment_script.value());
107+
108+
// PPLNS payout outputs (caller supplies final sorted order)
109+
for (auto& [script, amount] : payout_outputs)
110+
write_txout(amount, script);
111+
112+
// donation output
113+
write_txout(donation_amount, donation_script);
114+
115+
// OP_RETURN ref commitment (value 0)
116+
write_txout(0, op_return_script);
117+
118+
// lock_time = 0
119+
{
120+
uint32_t locktime = 0;
121+
tx.write(std::span<const std::byte>(reinterpret_cast<const std::byte*>(&locktime), 4));
122+
}
123+
124+
GentxCoinbase out;
125+
out.bytes.assign(reinterpret_cast<const unsigned char*>(tx.data()),
126+
reinterpret_cast<const unsigned char*>(tx.data()) + tx.size());
127+
auto sp = std::span<const unsigned char>(out.bytes.data(), out.bytes.size());
128+
out.txid = Hash(sp);
129+
return out;
130+
}
131+
132+
} // namespace btc::coin

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
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)
4+
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)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// BTC (#744 reconstructor slice 2/7) — SSOT non-witness coinbase (gentx) assembler KAT.
3+
//
4+
// Locks btc::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp) — the
5+
// single coinbase wire-layout extracted from generate_share_transaction()
6+
// (share_check.hpp:965, the verification SSOT) — against GROUND-TRUTH vectors
7+
// derived from the canonical oracle frstrtr/p2pool-dgb-scrypt
8+
// (util/pack.py byte logic + bitcoin/data.py tx_id_type; donation script
9+
// 4104ffd0...ac). The vectors are NOT self-generated from this builder: they
10+
// are the oracle serializer's output, so a PASS proves emission==oracle, not
11+
// merely self-consistency.
12+
//
13+
// Asserts, for both the no-segwit and witness-commitment-first layouts:
14+
// build -> non-witness serialize == oracle BYTES
15+
// double-SHA256(bytes) (GetHex display) == oracle TXID
16+
// Per-coin isolation: the only BTC<->DGB divergence is the segwit predicate at
17+
// serialize time (witness vout present-or-absent), exercised by both cases.
18+
19+
#include <gtest/gtest.h>
20+
#include <impl/btc/coin/gentx_coinbase.hpp>
21+
22+
#include <optional>
23+
#include <string>
24+
#include <utility>
25+
#include <vector>
26+
27+
namespace {
28+
29+
std::vector<unsigned char> unhex(const std::string& h) {
30+
std::vector<unsigned char> v; v.reserve(h.size() / 2);
31+
auto nyb = [](char c) -> int { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; };
32+
for (size_t i = 0; i + 1 < h.size(); i += 2)
33+
v.push_back(static_cast<unsigned char>((nyb(h[i]) << 4) | nyb(h[i + 1])));
34+
return v;
35+
}
36+
std::string tohex(const std::vector<unsigned char>& v) {
37+
static const char* H = "0123456789abcdef";
38+
std::string s; s.reserve(v.size() * 2);
39+
for (unsigned char b : v) { s.push_back(H[b >> 4]); s.push_back(H[b & 0xf]); }
40+
return s;
41+
}
42+
43+
// --- fixed inputs shared verbatim with the oracle generator ----------------
44+
const std::vector<unsigned char> CB = unhex("03a1b2c3041122334455667788");
45+
const std::vector<unsigned char> P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac");
46+
const std::vector<unsigned char> P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac");
47+
const std::vector<unsigned char> DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac");
48+
49+
const std::vector<std::pair<std::vector<unsigned char>, uint64_t>> PAYOUTS = {
50+
{P1, 5000000000ull},
51+
{P2, 2500000000ull},
52+
};
53+
54+
// Ground truth from the oracle serializer (see file header).
55+
const std::string NOSEG_BYTES =
56+
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
57+
const std::string NOSEG_TXID =
58+
"c5734775c1521b216e0e1bca506e4d15755cf55125caf56b7e0728a6d54a9b59";
59+
const std::string SEG_BYTES =
60+
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff050000000000000000266a24aa21a9edcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd00f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000";
61+
const std::string SEG_TXID =
62+
"4b66aabff52ca1336b52688815cece123075856880aaf6e77cb44f0d477b6162";
63+
64+
} // namespace
65+
66+
// op_return / wc literals: build them explicitly to avoid std::string surprises.
67+
static std::vector<unsigned char> make_opret() {
68+
auto v = unhex("6a28");
69+
for (int i = 0; i < 32; ++i) v.push_back(0xab);
70+
const unsigned char nonce[8] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01};
71+
for (unsigned char b : nonce) v.push_back(b);
72+
return v;
73+
}
74+
static std::vector<unsigned char> make_wc() {
75+
auto v = unhex("6a24aa21a9ed");
76+
for (int i = 0; i < 32; ++i) v.push_back(0xcd);
77+
return v;
78+
}
79+
80+
TEST(BTC_gentx_coinbase, NoSegwitOracleVector) {
81+
auto opret = make_opret();
82+
auto g = btc::coin::assemble_gentx_coinbase(
83+
CB, std::nullopt, PAYOUTS, /*donation_amount=*/1, DON, opret);
84+
EXPECT_EQ(tohex(g.bytes), NOSEG_BYTES);
85+
EXPECT_EQ(g.txid.GetHex(), NOSEG_TXID);
86+
}
87+
88+
TEST(BTC_gentx_coinbase, SegwitCommitmentFirstOracleVector) {
89+
auto opret = make_opret();
90+
auto wc = make_wc();
91+
auto g = btc::coin::assemble_gentx_coinbase(
92+
CB, std::optional<std::vector<unsigned char>>(wc), PAYOUTS,
93+
/*donation_amount=*/1, DON, opret);
94+
EXPECT_EQ(tohex(g.bytes), SEG_BYTES);
95+
EXPECT_EQ(g.txid.GetHex(), SEG_TXID);
96+
}
97+
98+
// Guard the per-coin isolation invariant: toggling only the segwit predicate
99+
// changes vout-count + prepends exactly one 0-value commitment vout, nothing
100+
// else — the divergence is gated, not a forked framer.
101+
TEST(BTC_gentx_coinbase, SegwitPredicateIsTheOnlyDivergence) {
102+
EXPECT_NE(NOSEG_BYTES, SEG_BYTES);
103+
// both share the identical coinbase-script vin prefix and donation/op_return tail
104+
EXPECT_EQ(NOSEG_BYTES.substr(0, 90), SEG_BYTES.substr(0, 90));
105+
}

0 commit comments

Comments
 (0)