|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// --------------------------------------------------------------------------- |
| 3 | +// btc_template_capture_test -- pins coin/template_capture.hpp, the per-job |
| 4 | +// template-retention seam: the PRODUCTION captured_template_txs_fn the won-block |
| 5 | +// reconstructor's template_other_txs_fn is built from (via the slice-5 |
| 6 | +// make_template_other_txs_fn bridge, not yet landed), replacing any interim |
| 7 | +// mempool RE-SELECTION path (merkle-consistent only on a static mempool). |
| 8 | +// |
| 9 | +// BTC reconstructor slice 3/7. This is a LEAF slice: TemplateCapture is opaque |
| 10 | +// to transactions[] content (it stores/replays nlohmann::json verbatim), so the |
| 11 | +// KAT pins the store contract WITHOUT the slice-5 bridge -- keeping the slice |
| 12 | +// dependency-free. The composed bridge round-trip lands with slice 5. |
| 13 | +// |
| 14 | +// Oracle vectors are coin-agnostic GBT transactions[] shape ({data,txid,hash, |
| 15 | +// fee}) hand-built as json literals -- the capture store treats them as opaque |
| 16 | +// json, so no coin codec / mempool dependency is needed to observe its bytes. |
| 17 | +// |
| 18 | +// What it pins: |
| 19 | +// * capture(share_hash, transactions[]) -> provide(share_hash) replays the |
| 20 | +// SAME GBT transactions[] json-faithfully (json-equal), keyed by share. |
| 21 | +// * a capture MISS replays an empty array -> coinbase-only valid block (NOT |
| 22 | +// fail-closed), so a missing template never forfeits the won subsidy. |
| 23 | +// * overwrite-same-hash keeps one entry (latest wins); FIFO eviction bounds |
| 24 | +// the store and drops the OLDEST template once over capacity. |
| 25 | +// |
| 26 | +// Rides the allowlisted btc_share_test add_executable (test/CMakeLists.txt); |
| 27 | +// a new *_test.cpp MUST be in that source list or it silently NOT_BUILT (#143). |
| 28 | +// Per-coin isolation: src/impl/btc/ only. |
| 29 | +// --------------------------------------------------------------------------- |
| 30 | +#include <cstddef> |
| 31 | +#include <string> |
| 32 | + |
| 33 | +#include <gtest/gtest.h> |
| 34 | + |
| 35 | +#include <nlohmann/json.hpp> |
| 36 | + |
| 37 | +#include <impl/btc/coin/template_capture.hpp> |
| 38 | +#include <core/uint256.hpp> |
| 39 | + |
| 40 | +using btc::coin::TemplateCapture; |
| 41 | + |
| 42 | +namespace { |
| 43 | + |
| 44 | +uint256 hash_for(const char* hex) |
| 45 | +{ |
| 46 | + uint256 h; h.SetHex(hex); |
| 47 | + return h; |
| 48 | +} |
| 49 | + |
| 50 | +// A production-shaped GBT transactions[]: array of {data,txid,hash,fee}. Content |
| 51 | +// is opaque to TemplateCapture -- these literals stand in for a populated GBT. |
| 52 | +nlohmann::json sample_template_txs() |
| 53 | +{ |
| 54 | + return nlohmann::json::array({ |
| 55 | + {{"data", "0100000001aa"}, {"txid", "aa01"}, {"hash", "aa01"}, {"fee", 700}}, |
| 56 | + {{"data", "0100000001bb"}, {"txid", "bb02"}, {"hash", "bb02"}, {"fee", 300}}, |
| 57 | + }); |
| 58 | +} |
| 59 | + |
| 60 | +const char* H_A = "00000000000000000000000000000000000000000000000000000000000000a1"; |
| 61 | +const char* H_B = "00000000000000000000000000000000000000000000000000000000000000b2"; |
| 62 | +const char* H_C = "00000000000000000000000000000000000000000000000000000000000000c3"; |
| 63 | +const char* H_MISS = "00000000000000000000000000000000000000000000000000000000deadbeef"; |
| 64 | + |
| 65 | +} // namespace |
| 66 | + |
| 67 | +// --- Test 1: capture -> provide round-trips the template json-faithfully ------- |
| 68 | +TEST(BtcTemplateCapture, CaptureProvideRoundTrip) |
| 69 | +{ |
| 70 | + TemplateCapture cap; |
| 71 | + const auto txs = sample_template_txs(); |
| 72 | + ASSERT_EQ(txs.size(), 2u); |
| 73 | + |
| 74 | + cap.capture(hash_for(H_A), txs); |
| 75 | + EXPECT_EQ(cap.size(), 1u); |
| 76 | + // json-equal: the exact transactions[] handed in comes back out. |
| 77 | + EXPECT_EQ(cap.provide(hash_for(H_A)), txs); |
| 78 | +} |
| 79 | + |
| 80 | +// --- Test 2: a MISS replays an empty array (coinbase-only, not fail-closed) --- |
| 81 | +TEST(BtcTemplateCapture, MissReplaysEmptyArray) |
| 82 | +{ |
| 83 | + TemplateCapture cap; |
| 84 | + cap.capture(hash_for(H_A), sample_template_txs()); |
| 85 | + |
| 86 | + const auto miss = cap.provide(hash_for(H_MISS)); |
| 87 | + ASSERT_TRUE(miss.is_array()); |
| 88 | + EXPECT_TRUE(miss.empty()); |
| 89 | +} |
| 90 | + |
| 91 | +// --- Test 3: the provider() closure replays HIT and MISS identically ---------- |
| 92 | +// Pins that provider() is a transparent view over provide(): a HIT yields the |
| 93 | +// captured template json-equal, a MISS yields an empty array. (The slice-5 |
| 94 | +// make_template_other_txs_fn bridge decodes this same output; that composed |
| 95 | +// round-trip lands with slice 5.) |
| 96 | +TEST(BtcTemplateCapture, ProviderClosureMirrorsProvide) |
| 97 | +{ |
| 98 | + TemplateCapture cap; |
| 99 | + const auto txs = sample_template_txs(); |
| 100 | + cap.capture(hash_for(H_A), txs); |
| 101 | + |
| 102 | + auto provider = cap.provider(); |
| 103 | + EXPECT_EQ(provider(hash_for(H_A)), txs); // HIT: json-equal |
| 104 | + EXPECT_TRUE(provider(hash_for(H_MISS)).empty()); // MISS: empty array |
| 105 | +} |
| 106 | + |
| 107 | +// --- Test 4: overwrite the same share_hash keeps one entry (latest wins) ------ |
| 108 | +TEST(BtcTemplateCapture, OverwriteSameHashLatestWins) |
| 109 | +{ |
| 110 | + TemplateCapture cap; |
| 111 | + cap.capture(hash_for(H_A), nlohmann::json::array()); // empty first |
| 112 | + cap.capture(hash_for(H_A), sample_template_txs()); // then 2-tx template |
| 113 | + EXPECT_EQ(cap.size(), 1u); // not duplicated |
| 114 | + EXPECT_EQ(cap.provide(hash_for(H_A)).size(), 2u); // latest content |
| 115 | +} |
| 116 | + |
| 117 | +// --- Test 5: FIFO eviction bounds the store, drops the OLDEST template -------- |
| 118 | +TEST(BtcTemplateCapture, BoundedFifoEvictsOldest) |
| 119 | +{ |
| 120 | + TemplateCapture cap(/*capacity=*/2); |
| 121 | + cap.capture(hash_for(H_A), sample_template_txs()); |
| 122 | + cap.capture(hash_for(H_B), sample_template_txs()); |
| 123 | + cap.capture(hash_for(H_C), sample_template_txs()); // evicts H_A |
| 124 | + |
| 125 | + EXPECT_EQ(cap.size(), 2u); |
| 126 | + EXPECT_TRUE(cap.provide(hash_for(H_A)).empty()); // oldest evicted -> miss |
| 127 | + EXPECT_EQ(cap.provide(hash_for(H_B)).size(), 2u); // newest retained |
| 128 | + EXPECT_EQ(cap.provide(hash_for(H_C)).size(), 2u); |
| 129 | +} |
0 commit comments