|
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// dgb_template_other_txs_test -- pins coin/template_other_txs.hpp, the producer |
| 3 | +// bridge that decodes the embedded work template's transactions[] (the GBT data |
| 4 | +// the miner is funded with, shaped by make_mempool_tx_source) back into the |
| 5 | +// MutableTransaction vector the won-block reconstructor's template_other_txs_fn |
| 6 | +// seam frames as [gentx] ++ other_txs. |
| 7 | +// |
| 8 | +// This closes the loop between the two already-pinned halves: |
| 9 | +// * make_mempool_tx_source (embedded_tx_select.cpp) -- Mempool -> GBT txs[] |
| 10 | +// * make_reconstruct_closure_from_template (#82) -- [gentx] ++ other_txs |
| 11 | +// proving the SAME txs the template hands the miner are the txs that land in the |
| 12 | +// reconstructed broadcast block, byte-faithfully and in template order. |
| 13 | +// |
| 14 | +// Links the full dgb_coin codec (it compiles the tx serialization) and the |
| 15 | +// reconstruct closure. MUST be in BOTH build.yml --target allowlists (#143 |
| 16 | +// NOT_BUILT trap). Per-coin isolation: src/impl/dgb/ only. |
| 17 | +// --------------------------------------------------------------------------- |
| 18 | +#include <cstdint> |
| 19 | +#include <functional> |
| 20 | +#include <stdexcept> |
| 21 | +#include <string> |
| 22 | +#include <vector> |
| 23 | + |
| 24 | +#include <gtest/gtest.h> |
| 25 | + |
| 26 | +#include <impl/dgb/coin/template_other_txs.hpp> |
| 27 | +#include <impl/dgb/coin/embedded_tx_select.hpp> |
| 28 | +#include <impl/dgb/coin/mempool.hpp> |
| 29 | +#include <impl/dgb/coin/transaction.hpp> |
| 30 | +#include <impl/dgb/coin/reconstruct_closure.hpp> |
| 31 | + |
| 32 | +#include <core/pack.hpp> |
| 33 | +#include <core/uint256.hpp> |
| 34 | +#include <btclibs/util/strencodings.h> |
| 35 | + |
| 36 | +using dgb::coin::Mempool; |
| 37 | +using dgb::coin::MutableTransaction; |
| 38 | +using dgb::coin::TxIn; |
| 39 | +using dgb::coin::TxOut; |
| 40 | +using dgb::coin::TX_WITH_WITNESS; |
| 41 | +using dgb::coin::TX_NO_WITNESS; |
| 42 | +using dgb::coin::compute_txid; |
| 43 | +using dgb::coin::make_mempool_tx_source; |
| 44 | +using dgb::coin::deserialize_template_tx; |
| 45 | +using dgb::coin::deserialize_template_other_txs; |
| 46 | +using dgb::coin::make_template_other_txs_fn; |
| 47 | +using dgb::coin::make_reconstruct_closure_from_template; |
| 48 | +using dgb::coin::reconstruct_won_block_from_template; |
| 49 | +using dgb::coin::unpack_gentx_coinbase; |
| 50 | +using dgb::coin::SmallBlockHeaderType; |
| 51 | +using dgb::coin::WonShareInputs; |
| 52 | + |
| 53 | +namespace { |
| 54 | + |
| 55 | +MutableTransaction tagged_tx(int64_t value, uint32_t index) |
| 56 | +{ |
| 57 | + MutableTransaction tx; |
| 58 | + tx.version = 1; |
| 59 | + tx.locktime = 0; |
| 60 | + TxIn in; |
| 61 | + in.prevout.hash.SetNull(); |
| 62 | + in.prevout.index = index; |
| 63 | + in.sequence = 0xffffffff; |
| 64 | + tx.vin.push_back(in); |
| 65 | + TxOut out; |
| 66 | + out.value = value; |
| 67 | + tx.vout.push_back(out); |
| 68 | + return tx; |
| 69 | +} |
| 70 | + |
| 71 | +MutableTransaction make_gentx() |
| 72 | +{ |
| 73 | + MutableTransaction tx; |
| 74 | + tx.version = 1; |
| 75 | + tx.locktime = 0; |
| 76 | + TxIn in; |
| 77 | + in.prevout.hash.SetNull(); |
| 78 | + in.prevout.index = 0xffffffff; // coinbase |
| 79 | + in.sequence = 0xffffffff; |
| 80 | + tx.vin.push_back(in); |
| 81 | + TxOut out; |
| 82 | + out.value = 5000000000LL; |
| 83 | + tx.vout.push_back(out); |
| 84 | + return tx; |
| 85 | +} |
| 86 | + |
| 87 | +std::vector<unsigned char> noseg_bytes(const MutableTransaction& tx) |
| 88 | +{ |
| 89 | + auto packed = pack(TX_NO_WITNESS(tx)); |
| 90 | + auto sp = packed.get_span(); |
| 91 | + return std::vector<unsigned char>( |
| 92 | + reinterpret_cast<const unsigned char*>(sp.data()), |
| 93 | + reinterpret_cast<const unsigned char*>(sp.data()) + sp.size()); |
| 94 | +} |
| 95 | + |
| 96 | +std::string withwit_hex(const MutableTransaction& tx) |
| 97 | +{ |
| 98 | + return HexStr(pack(TX_WITH_WITNESS(tx)).get_span()); |
| 99 | +} |
| 100 | + |
| 101 | +SmallBlockHeaderType make_small_header() |
| 102 | +{ |
| 103 | + SmallBlockHeaderType h; |
| 104 | + h.m_version = 0x20000000; |
| 105 | + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); |
| 106 | + h.m_timestamp = 1718700000; |
| 107 | + h.m_bits = 0x1a0fffff; |
| 108 | + h.m_nonce = 0x12345678; |
| 109 | + return h; |
| 110 | +} |
| 111 | + |
| 112 | +uint256 won_hash() |
| 113 | +{ |
| 114 | + uint256 h; h.SetHex("00000000000000000000000000000000000000000000000000000000000000a0"); |
| 115 | + return h; |
| 116 | +} |
| 117 | + |
| 118 | +} // namespace |
| 119 | + |
| 120 | +// --- Test 1: round-trip -- the production template txs decode byte-faithfully - |
| 121 | +// Populate a Mempool, shape it through the PRODUCTION make_mempool_tx_source, |
| 122 | +// then decode the resulting transactions[] back through the bridge. Each decoded |
| 123 | +// tx must re-serialize (with-witness) to the exact `data` the template carried |
| 124 | +// and carry the exact `txid`, in template order. |
| 125 | +TEST(DgbTemplateOtherTxs, RoundTripsProductionTemplateTxs) |
| 126 | +{ |
| 127 | + Mempool pool; |
| 128 | + MutableTransaction a = tagged_tx(10, 0); |
| 129 | + MutableTransaction b = tagged_tx(20, 1); |
| 130 | + ASSERT_TRUE(pool.add_tx(a)); |
| 131 | + ASSERT_TRUE(pool.add_tx(b)); |
| 132 | + pool.set_tx_fee(compute_txid(a), 700); |
| 133 | + pool.set_tx_fee(compute_txid(b), 300); |
| 134 | + |
| 135 | + auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000); |
| 136 | + const auto sel = source(); |
| 137 | + ASSERT_EQ(sel.transactions.size(), 2u); |
| 138 | + |
| 139 | + const auto txs = deserialize_template_other_txs(sel.transactions); |
| 140 | + ASSERT_EQ(txs.size(), sel.transactions.size()); // template order preserved |
| 141 | + |
| 142 | + for (size_t i = 0; i < txs.size(); ++i) |
| 143 | + { |
| 144 | + // Decoded tx re-serializes to the exact template `data` and `txid`. |
| 145 | + EXPECT_EQ(withwit_hex(txs[i]), sel.transactions[i]["data"].get<std::string>()); |
| 146 | + EXPECT_EQ(compute_txid(txs[i]).GetHex(), sel.transactions[i]["txid"].get<std::string>()); |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ---- |
| 151 | +TEST(DgbTemplateOtherTxs, EmptyTransactionsIsEmptyVector) |
| 152 | +{ |
| 153 | + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty()); |
| 154 | + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty()); |
| 155 | +} |
| 156 | + |
| 157 | +// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) --------- |
| 158 | +TEST(DgbTemplateOtherTxs, TrailingBytesThrow) |
| 159 | +{ |
| 160 | + std::string good = withwit_hex(tagged_tx(10, 0)); |
| 161 | + EXPECT_NO_THROW(deserialize_template_tx(good)); |
| 162 | + EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte |
| 163 | +} |
| 164 | + |
| 165 | +// --- Test 4: end-to-end -- funded template txs land in the reconstructed block - |
| 166 | +// The bridge, wired as the reconstructor's template_other_txs_fn via a per-share |
| 167 | +// captured-transactions[] provider, reconstructs the SAME block that feeding the |
| 168 | +// deserialized txs straight into reconstruct_won_block_from_template produces: |
| 169 | +// [gentx] ++ the mempool-funded template txs, byte-identical. |
| 170 | +TEST(DgbTemplateOtherTxs, ReconstructsBlockWithFundedTemplateTxs) |
| 171 | +{ |
| 172 | + Mempool pool; |
| 173 | + MutableTransaction a = tagged_tx(10, 0); |
| 174 | + MutableTransaction b = tagged_tx(20, 1); |
| 175 | + ASSERT_TRUE(pool.add_tx(a)); |
| 176 | + ASSERT_TRUE(pool.add_tx(b)); |
| 177 | + pool.set_tx_fee(compute_txid(a), 700); |
| 178 | + pool.set_tx_fee(compute_txid(b), 300); |
| 179 | + const auto sel = make_mempool_tx_source(pool, /*max_weight=*/4'000'000)(); |
| 180 | + ASSERT_FALSE(sel.transactions.empty()); |
| 181 | + |
| 182 | + auto sh = make_small_header(); |
| 183 | + ::dgb::MerkleLink link; |
| 184 | + const uint256 won = won_hash(); |
| 185 | + auto gentx_bytes = noseg_bytes(make_gentx()); |
| 186 | + auto ug = unpack_gentx_coinbase(gentx_bytes); |
| 187 | + |
| 188 | + // Expected: [gentx] ++ decoded-template-txs straight through the SSOT. |
| 189 | + const auto other = deserialize_template_other_txs(sel.transactions); |
| 190 | + const auto expected = |
| 191 | + reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other); |
| 192 | + |
| 193 | + // Actual: the run-loop shape -- closure pulls the per-share captured txs[] |
| 194 | + // through the bridge factory. |
| 195 | + auto txs_json = sel.transactions; |
| 196 | + auto closure = make_reconstruct_closure_from_template( |
| 197 | + [won, sh, link](const uint256& h) -> WonShareInputs { |
| 198 | + if (h != won) throw std::out_of_range("unknown share"); |
| 199 | + return WonShareInputs{sh, link}; |
| 200 | + }, |
| 201 | + [gentx_bytes](const uint256&) { return gentx_bytes; }, |
| 202 | + make_template_other_txs_fn( |
| 203 | + [txs_json](const uint256&) { return txs_json; })); |
| 204 | + |
| 205 | + auto got = closure(won); |
| 206 | + ASSERT_TRUE(got.has_value()); |
| 207 | + EXPECT_FALSE(got->first.empty()); |
| 208 | + EXPECT_EQ(got->first, expected.bytes); // funded txs land in the block |
| 209 | + EXPECT_EQ(got->second, expected.hex); |
| 210 | +} |
| 211 | + |
| 212 | +// --- Test 5: a bad captured provider fails the whole won block CLOSED ---------- |
| 213 | +TEST(DgbTemplateOtherTxs, BadProviderFailsClosed) |
| 214 | +{ |
| 215 | + auto sh = make_small_header(); |
| 216 | + ::dgb::MerkleLink link; |
| 217 | + const uint256 won = won_hash(); |
| 218 | + auto gentx_bytes = noseg_bytes(make_gentx()); |
| 219 | + |
| 220 | + nlohmann::json bad = nlohmann::json::array(); |
| 221 | + bad.push_back({{"data", withwit_hex(tagged_tx(10, 0)) + "ff"}}); // trailing junk |
| 222 | + |
| 223 | + auto closure = make_reconstruct_closure_from_template( |
| 224 | + [won, sh, link](const uint256& h) -> WonShareInputs { |
| 225 | + if (h != won) throw std::out_of_range("unknown share"); |
| 226 | + return WonShareInputs{sh, link}; |
| 227 | + }, |
| 228 | + [gentx_bytes](const uint256&) { return gentx_bytes; }, |
| 229 | + make_template_other_txs_fn([bad](const uint256&) { return bad; })); |
| 230 | + |
| 231 | + EXPECT_FALSE(closure(won).has_value()); // decode throw -> nullopt, no broadcast |
| 232 | +} |
0 commit comments