|
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// dgb_template_capture_test -- pins coin/template_capture.hpp, the per-job |
| 3 | +// template-retention seam (#271): the PRODUCTION captured_template_txs_fn the |
| 4 | +// won-block reconstructor's template_other_txs_fn is built from (via the #299 |
| 5 | +// make_template_other_txs_fn bridge), replacing main_dgb's interim mempool |
| 6 | +// RE-SELECTION lambda (merkle-consistent only on a static mempool). |
| 7 | +// |
| 8 | +// What it pins: |
| 9 | +// * capture(share_hash, transactions[]) -> provide(share_hash) replays the |
| 10 | +// SAME GBT transactions[] byte-faithfully (json-equal), keyed by share. |
| 11 | +// * a capture MISS replays an empty array -> coinbase-only valid block (NOT |
| 12 | +// fail-closed), so a missing template never forfeits the won subsidy. |
| 13 | +// * make_template_other_txs_fn(capture.provider()) decodes a HIT to the exact |
| 14 | +// MutableTransaction vector deserialize_template_other_txs yields directly |
| 15 | +// from the same template -- i.e. the capture path is transparent to the |
| 16 | +// #299 bridge -- and a MISS to an empty other_txs vector. |
| 17 | +// * overwrite-same-hash keeps one entry (latest wins); FIFO eviction bounds |
| 18 | +// the store and drops the OLDEST template once over capacity. |
| 19 | +// |
| 20 | +// Links the full dgb_coin codec + reconstruct closure (it composes the #299 |
| 21 | +// bridge over the production make_mempool_tx_source template). MUST be in BOTH |
| 22 | +// build.yml --target allowlists (#143 NOT_BUILT trap). Per-coin isolation: |
| 23 | +// src/impl/dgb/ only. |
| 24 | +// --------------------------------------------------------------------------- |
| 25 | +#include <cstdint> |
| 26 | +#include <functional> |
| 27 | +#include <string> |
| 28 | +#include <vector> |
| 29 | + |
| 30 | +#include <gtest/gtest.h> |
| 31 | + |
| 32 | +#include <impl/dgb/coin/template_capture.hpp> |
| 33 | +#include <impl/dgb/coin/template_other_txs.hpp> |
| 34 | +#include <impl/dgb/coin/embedded_tx_select.hpp> |
| 35 | +#include <impl/dgb/coin/mempool.hpp> |
| 36 | +#include <impl/dgb/coin/transaction.hpp> |
| 37 | + |
| 38 | +#include <core/pack.hpp> |
| 39 | +#include <core/uint256.hpp> |
| 40 | +#include <btclibs/util/strencodings.h> |
| 41 | + |
| 42 | +using dgb::coin::Mempool; |
| 43 | +using dgb::coin::MutableTransaction; |
| 44 | +using dgb::coin::TxIn; |
| 45 | +using dgb::coin::TxOut; |
| 46 | +using dgb::coin::TX_WITH_WITNESS; |
| 47 | +using dgb::coin::compute_txid; |
| 48 | +using dgb::coin::make_mempool_tx_source; |
| 49 | +using dgb::coin::deserialize_template_other_txs; |
| 50 | +using dgb::coin::make_template_other_txs_fn; |
| 51 | +using dgb::coin::TemplateCapture; |
| 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 | +std::string withwit_hex(const MutableTransaction& tx) |
| 72 | +{ |
| 73 | + return HexStr(pack(TX_WITH_WITNESS(tx)).get_span()); |
| 74 | +} |
| 75 | + |
| 76 | +uint256 hash_for(const char* hex) |
| 77 | +{ |
| 78 | + uint256 h; h.SetHex(hex); |
| 79 | + return h; |
| 80 | +} |
| 81 | + |
| 82 | +// A production-shaped GBT transactions[] from a populated mempool. |
| 83 | +nlohmann::json sample_template_txs() |
| 84 | +{ |
| 85 | + Mempool pool; |
| 86 | + MutableTransaction a = tagged_tx(11, 0); |
| 87 | + MutableTransaction b = tagged_tx(22, 1); |
| 88 | + pool.add_tx(a); |
| 89 | + pool.add_tx(b); |
| 90 | + pool.set_tx_fee(compute_txid(a), 700); |
| 91 | + pool.set_tx_fee(compute_txid(b), 300); |
| 92 | + return make_mempool_tx_source(pool, /*max_weight=*/4'000'000)().transactions; |
| 93 | +} |
| 94 | + |
| 95 | +const char* H_A = "00000000000000000000000000000000000000000000000000000000000000a1"; |
| 96 | +const char* H_B = "00000000000000000000000000000000000000000000000000000000000000b2"; |
| 97 | +const char* H_C = "00000000000000000000000000000000000000000000000000000000000000c3"; |
| 98 | +const char* H_MISS = "00000000000000000000000000000000000000000000000000000000deadbeef"; |
| 99 | + |
| 100 | +} // namespace |
| 101 | + |
| 102 | +// --- Test 1: capture -> provide round-trips the template byte-faithfully ------ |
| 103 | +TEST(DgbTemplateCapture, CaptureProvideRoundTrip) |
| 104 | +{ |
| 105 | + TemplateCapture cap; |
| 106 | + const auto txs = sample_template_txs(); |
| 107 | + ASSERT_EQ(txs.size(), 2u); |
| 108 | + |
| 109 | + cap.capture(hash_for(H_A), txs); |
| 110 | + EXPECT_EQ(cap.size(), 1u); |
| 111 | + // json-equal: the exact transactions[] handed in comes back out. |
| 112 | + EXPECT_EQ(cap.provide(hash_for(H_A)), txs); |
| 113 | +} |
| 114 | + |
| 115 | +// --- Test 2: a MISS replays an empty array (coinbase-only, not fail-closed) --- |
| 116 | +TEST(DgbTemplateCapture, MissReplaysEmptyArray) |
| 117 | +{ |
| 118 | + TemplateCapture cap; |
| 119 | + cap.capture(hash_for(H_A), sample_template_txs()); |
| 120 | + |
| 121 | + const auto miss = cap.provide(hash_for(H_MISS)); |
| 122 | + ASSERT_TRUE(miss.is_array()); |
| 123 | + EXPECT_TRUE(miss.empty()); |
| 124 | +} |
| 125 | + |
| 126 | +// --- Test 3: composed through the #299 bridge, a HIT decodes to the exact tx -- |
| 127 | +// vector deserialize_template_other_txs yields directly from the same template; |
| 128 | +// a MISS decodes to an empty other_txs vector. Proves the capture path is |
| 129 | +// transparent to the reconstructor's template_other_txs_fn. |
| 130 | +TEST(DgbTemplateCapture, ComposedWithBridgeHitAndMiss) |
| 131 | +{ |
| 132 | + TemplateCapture cap; |
| 133 | + const auto txs = sample_template_txs(); |
| 134 | + cap.capture(hash_for(H_A), txs); |
| 135 | + |
| 136 | + auto other_txs_fn = make_template_other_txs_fn(cap.provider()); |
| 137 | + |
| 138 | + // HIT: identical to decoding the template directly, in template order. |
| 139 | + const auto via_capture = other_txs_fn(hash_for(H_A)); |
| 140 | + const auto direct = deserialize_template_other_txs(txs); |
| 141 | + ASSERT_EQ(via_capture.size(), direct.size()); |
| 142 | + ASSERT_EQ(via_capture.size(), 2u); |
| 143 | + for (size_t i = 0; i < via_capture.size(); ++i) |
| 144 | + EXPECT_EQ(withwit_hex(via_capture[i]), withwit_hex(direct[i])); |
| 145 | + |
| 146 | + // MISS: empty other_txs -> the reconstructor frames a coinbase-only block. |
| 147 | + EXPECT_TRUE(other_txs_fn(hash_for(H_MISS)).empty()); |
| 148 | +} |
| 149 | + |
| 150 | +// --- Test 4: overwrite the same share_hash keeps one entry (latest wins) ------ |
| 151 | +TEST(DgbTemplateCapture, OverwriteSameHashLatestWins) |
| 152 | +{ |
| 153 | + TemplateCapture cap; |
| 154 | + cap.capture(hash_for(H_A), nlohmann::json::array()); // empty first |
| 155 | + cap.capture(hash_for(H_A), sample_template_txs()); // then 2-tx template |
| 156 | + EXPECT_EQ(cap.size(), 1u); // not duplicated |
| 157 | + EXPECT_EQ(cap.provide(hash_for(H_A)).size(), 2u); // latest content |
| 158 | +} |
| 159 | + |
| 160 | +// --- Test 5: FIFO eviction bounds the store, drops the OLDEST template -------- |
| 161 | +TEST(DgbTemplateCapture, BoundedFifoEvictsOldest) |
| 162 | +{ |
| 163 | + TemplateCapture cap(/*capacity=*/2); |
| 164 | + cap.capture(hash_for(H_A), sample_template_txs()); |
| 165 | + cap.capture(hash_for(H_B), sample_template_txs()); |
| 166 | + cap.capture(hash_for(H_C), sample_template_txs()); // evicts H_A |
| 167 | + |
| 168 | + EXPECT_EQ(cap.size(), 2u); |
| 169 | + EXPECT_TRUE(cap.provide(hash_for(H_A)).empty()); // oldest evicted -> miss |
| 170 | + EXPECT_EQ(cap.provide(hash_for(H_B)).size(), 2u); // newest retained |
| 171 | + EXPECT_EQ(cap.provide(hash_for(H_C)).size(), 2u); |
| 172 | +} |
0 commit comments