|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// --------------------------------------------------------------------------- |
| 3 | +// btc_template_other_txs (rides btc_share_test) -- pins |
| 4 | +// coin/template_other_txs.hpp, the producer bridge that decodes the captured |
| 5 | +// GBT work template's transactions[] (the conformant {data,txid,hash,fee} shape |
| 6 | +// bitcoind's getblocktemplate emits, retained per-job by TemplateCapture / #837) |
| 7 | +// back into the MutableTransaction vector the won-block reconstructor's |
| 8 | +// template_other_txs_fn seam frames as [gentx] ++ other_txs (#839). |
| 9 | +// |
| 10 | +// This closes the decode half of the loop: the SAME txs the captured template |
| 11 | +// carried are the txs that land in the reconstructed broadcast block, byte- |
| 12 | +// faithfully and in template order. The header depends ONLY on transaction.hpp, |
| 13 | +// so this KAT stands alone off master (no reconstruct-closure / TemplateCapture |
| 14 | +// link -- those slices land separately; the run-loop composition is slice 7). |
| 15 | +// |
| 16 | +// Rides the already-allowlisted btc_share_test executable, so no build.yml |
| 17 | +// --target change is needed (#143 NOT_BUILT trap). p2pool-merged-v36 surface: |
| 18 | +// NONE. Per-coin isolation: src/impl/btc/ only. |
| 19 | +// --------------------------------------------------------------------------- |
| 20 | + |
| 21 | +#include <gtest/gtest.h> |
| 22 | + |
| 23 | +#include <cstdint> |
| 24 | +#include <functional> |
| 25 | +#include <stdexcept> |
| 26 | +#include <string> |
| 27 | +#include <vector> |
| 28 | + |
| 29 | +#include <nlohmann/json.hpp> |
| 30 | + |
| 31 | +#include <core/pack.hpp> |
| 32 | +#include <core/hash.hpp> |
| 33 | +#include <core/uint256.hpp> |
| 34 | +#include <btclibs/util/strencodings.h> |
| 35 | + |
| 36 | +#include <impl/btc/coin/template_other_txs.hpp> |
| 37 | +#include <impl/btc/coin/transaction.hpp> |
| 38 | + |
| 39 | +namespace { |
| 40 | + |
| 41 | +using btc::coin::MutableTransaction; |
| 42 | +using btc::coin::TxIn; |
| 43 | +using btc::coin::TxOut; |
| 44 | +using btc::coin::TX_WITH_WITNESS; |
| 45 | +using btc::coin::TX_NO_WITNESS; |
| 46 | +using btc::coin::deserialize_template_tx; |
| 47 | +using btc::coin::deserialize_template_other_txs; |
| 48 | +using btc::coin::make_template_other_txs_fn; |
| 49 | + |
| 50 | +// A minimal witnessless payment tx tagged by output value + prevout index, so |
| 51 | +// two entries in one template are distinguishable after the round trip. |
| 52 | +MutableTransaction tagged_tx(int64_t value, uint32_t index) |
| 53 | +{ |
| 54 | + MutableTransaction tx; |
| 55 | + tx.version = 1; |
| 56 | + tx.locktime = 0; |
| 57 | + TxIn in; |
| 58 | + in.prevout.hash.SetNull(); |
| 59 | + in.prevout.index = index; |
| 60 | + in.sequence = 0xffffffff; |
| 61 | + tx.vin.push_back(in); |
| 62 | + TxOut out; |
| 63 | + out.value = value; |
| 64 | + tx.vout.push_back(out); |
| 65 | + return tx; |
| 66 | +} |
| 67 | + |
| 68 | +// The with-witness `data` hex the GBT template carries (== non-witness bytes for |
| 69 | +// a witnessless tx: no segwit marker is emitted when HasWitness()==false). |
| 70 | +// Single-expression: the temporary PackStream lives to the end of the full |
| 71 | +// expression, so get_span() does not dangle (cf. reconstruct_test.cpp note). |
| 72 | +std::string withwit_hex(const MutableTransaction& tx) |
| 73 | +{ |
| 74 | + return HexStr(pack(TX_WITH_WITNESS(tx)).get_span()); |
| 75 | +} |
| 76 | + |
| 77 | +uint256 txid(const MutableTransaction& tx) |
| 78 | +{ |
| 79 | + return Hash(pack(TX_NO_WITNESS(tx)).get_span()); |
| 80 | +} |
| 81 | + |
| 82 | +// Emulate the captured GBT transactions[]: an ordered array of {data,txid}. |
| 83 | +nlohmann::json gbt_entry(const MutableTransaction& tx) |
| 84 | +{ |
| 85 | + nlohmann::json e; |
| 86 | + e["data"] = withwit_hex(tx); |
| 87 | + e["txid"] = txid(tx).GetHex(); |
| 88 | + return e; |
| 89 | +} |
| 90 | + |
| 91 | +} // namespace |
| 92 | + |
| 93 | +// --- Test 1: round-trip -- template txs decode byte-faithfully, in order ------ |
| 94 | +// Each decoded tx must re-serialize (with-witness) to the exact `data` the |
| 95 | +// template carried and carry the exact txid, in template order. |
| 96 | +TEST(BtcTemplateOtherTxs, RoundTripsTemplateTxs) |
| 97 | +{ |
| 98 | + MutableTransaction a = tagged_tx(10, 0); |
| 99 | + MutableTransaction b = tagged_tx(20, 1); |
| 100 | + nlohmann::json transactions = nlohmann::json::array({gbt_entry(a), gbt_entry(b)}); |
| 101 | + |
| 102 | + const auto txs = deserialize_template_other_txs(transactions); |
| 103 | + ASSERT_EQ(txs.size(), 2u); // template order preserved |
| 104 | + |
| 105 | + EXPECT_EQ(withwit_hex(txs[0]), transactions[0]["data"].get<std::string>()); |
| 106 | + EXPECT_EQ(txid(txs[0]).GetHex(), transactions[0]["txid"].get<std::string>()); |
| 107 | + EXPECT_EQ(withwit_hex(txs[1]), transactions[1]["data"].get<std::string>()); |
| 108 | + EXPECT_EQ(txid(txs[1]).GetHex(), transactions[1]["txid"].get<std::string>()); |
| 109 | + // Distinguishable -> order really is [a, b], not swapped. |
| 110 | + EXPECT_EQ(txs[0].vout[0].value, 10); |
| 111 | + EXPECT_EQ(txs[1].vout[0].value, 20); |
| 112 | +} |
| 113 | + |
| 114 | +// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ---- |
| 115 | +TEST(BtcTemplateOtherTxs, EmptyTransactionsIsEmptyVector) |
| 116 | +{ |
| 117 | + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty()); |
| 118 | + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty()); |
| 119 | +} |
| 120 | + |
| 121 | +// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) --------- |
| 122 | +TEST(BtcTemplateOtherTxs, TrailingBytesThrow) |
| 123 | +{ |
| 124 | + const std::string good = withwit_hex(tagged_tx(10, 0)); |
| 125 | + EXPECT_NO_THROW(deserialize_template_tx(good)); |
| 126 | + EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte |
| 127 | + // ... and it fails the whole array closed, not just the bad entry. |
| 128 | + nlohmann::json transactions = nlohmann::json::array(); |
| 129 | + transactions.push_back({{"data", good + "ff"}}); |
| 130 | + EXPECT_THROW(deserialize_template_other_txs(transactions), std::out_of_range); |
| 131 | +} |
| 132 | + |
| 133 | +// --- Test 4: provider factory -- the run-loop wire shape ---------------------- |
| 134 | +// make_template_other_txs_fn wraps a per-share captured-transactions[] provider |
| 135 | +// (TemplateCapture::provider() shape) into the template_other_txs_fn seam. A hit |
| 136 | +// decodes the retained template; a miss (empty array, TemplateCapture's |
| 137 | +// documented miss policy) yields a coinbase-only empty vector. |
| 138 | +TEST(BtcTemplateOtherTxs, ProviderFactoryDecodesCapturedTemplate) |
| 139 | +{ |
| 140 | + MutableTransaction a = tagged_tx(30, 0); |
| 141 | + nlohmann::json captured = nlohmann::json::array({gbt_entry(a)}); |
| 142 | + |
| 143 | + uint256 won; won.SetHex("00000000000000000000000000000000000000000000000000000000000000a0"); |
| 144 | + uint256 other; other.SetHex("00000000000000000000000000000000000000000000000000000000000000b0"); |
| 145 | + |
| 146 | + auto fn = make_template_other_txs_fn( |
| 147 | + [won, captured](const uint256& h) -> nlohmann::json { |
| 148 | + return h == won ? captured : nlohmann::json::array(); // miss -> empty |
| 149 | + }); |
| 150 | + |
| 151 | + const auto hit = fn(won); |
| 152 | + ASSERT_EQ(hit.size(), 1u); |
| 153 | + EXPECT_EQ(hit[0].vout[0].value, 30); |
| 154 | + EXPECT_EQ(withwit_hex(hit[0]), captured[0]["data"].get<std::string>()); |
| 155 | + |
| 156 | + EXPECT_TRUE(fn(other).empty()); // coinbase-only on capture miss |
| 157 | +} |
0 commit comments