|
| 1 | +// F11 value-invariance KAT — guards the canonical exclude-then-append donation |
| 2 | +// handling in the BTC v36 payout sort (src/impl/btc/share_check.hpp, |
| 3 | +// build_payout_outputs_excluding_donation). |
| 4 | +// |
| 5 | +// Backfills the coverage gap that let PR-0 S1 (133ae6bc) silently revert the |
| 6 | +// LTC F11 (18dd9457) on a stale base: there was no test asserting the parity |
| 7 | +// arithmetic, so the regression shipped unnoticed. This BTC twin asserts the |
| 8 | +// same invariant set against its own helper + the core::donation SSOT bytes. |
| 9 | +// |
| 10 | +// Invariant under test (mirrors p2pool data.py generate_transaction): |
| 11 | +// - per-miner payout dests EXCLUDE both donation scripts (COMBINED P2SH + P2PK) |
| 12 | +// - COMBINED_DONATION_SCRIPT-keyed weight FOLDS into the single donation-last |
| 13 | +// output (moved, not destroyed) |
| 14 | +// - DONATION_SCRIPT (P2PK)-keyed weight is DROPPED — value-neutral ONLY because |
| 15 | +// that key never accrues weight in canonical v36 operation |
| 16 | +// - total coinbase value out == subsidy (no value created/destroyed) |
| 17 | +// |
| 18 | +// Coordinated with the LTC source (ltc/test/f11_donation_invariance_test.cpp); |
| 19 | +// only the donation-constant source (core::donation SSOT) and the helper coin |
| 20 | +// namespace (btc::) differ. |
| 21 | + |
| 22 | +#include <gtest/gtest.h> |
| 23 | + |
| 24 | +#include <map> |
| 25 | +#include <vector> |
| 26 | +#include <cstdint> |
| 27 | + |
| 28 | +#include <core/donation.hpp> |
| 29 | +#include <impl/btc/share_check.hpp> |
| 30 | + |
| 31 | +namespace { |
| 32 | + |
| 33 | +using Script = std::vector<unsigned char>; |
| 34 | +using Amounts = std::map<Script, uint64_t>; |
| 35 | + |
| 36 | +Script combined_script() { |
| 37 | + return Script(core::donation::COMBINED_DONATION_SCRIPT.begin(), |
| 38 | + core::donation::COMBINED_DONATION_SCRIPT.end()); |
| 39 | +} |
| 40 | +Script p2pk_script() { |
| 41 | + return Script(core::donation::DONATION_SCRIPT.begin(), |
| 42 | + core::donation::DONATION_SCRIPT.end()); |
| 43 | +} |
| 44 | +// Distinct miner payout scripts (P2PKH-shaped; bytes are arbitrary but != donation). |
| 45 | +Script miner(unsigned char tag) { |
| 46 | + return Script{0x76, 0xa9, 0x14, tag, tag, tag, tag, tag, tag, tag, tag, tag, tag, |
| 47 | + tag, tag, tag, tag, tag, tag, tag, tag, tag, tag, 0x88, 0xac}; |
| 48 | +} |
| 49 | + |
| 50 | +uint64_t sum_amounts(const Amounts& a) { |
| 51 | + uint64_t s = 0; for (auto& kv : a) s += kv.second; return s; |
| 52 | +} |
| 53 | +uint64_t sum_outputs(const std::vector<std::pair<Script, uint64_t>>& o) { |
| 54 | + uint64_t s = 0; for (auto& kv : o) s += kv.second; return s; |
| 55 | +} |
| 56 | +bool contains_script(const std::vector<std::pair<Script, uint64_t>>& o, const Script& s) { |
| 57 | + for (auto& kv : o) if (kv.first == s) return true; |
| 58 | + return false; |
| 59 | +} |
| 60 | + |
| 61 | +// Reproduces the production pre-fold accounting at each gentx site: |
| 62 | +// donation_amount = subsidy - sum(amounts), then the helper folds COMBINED in. |
| 63 | +struct FoldResult { |
| 64 | + std::vector<std::pair<Script, uint64_t>> payout_outputs; |
| 65 | + uint64_t donation_amount; |
| 66 | +}; |
| 67 | +FoldResult run_fold(const Amounts& amounts, uint64_t subsidy) { |
| 68 | + uint64_t sa = sum_amounts(amounts); |
| 69 | + uint64_t donation_amount = (subsidy > sa) ? (subsidy - sa) : 0; |
| 70 | + auto outs = btc::build_payout_outputs_excluding_donation( |
| 71 | + amounts, combined_script(), p2pk_script(), donation_amount); |
| 72 | + return {outs, donation_amount}; |
| 73 | +} |
| 74 | + |
| 75 | +} // namespace |
| 76 | + |
| 77 | +// Canonical case: COMBINED weight present, no P2PK weight. The fold must be |
| 78 | +// fully value-invariant — every satoshi of subsidy is accounted for. |
| 79 | +TEST(BTC_F11_DonationInvariance, CombinedFoldedNoP2PK) { |
| 80 | + const uint64_t subsidy = 1000; |
| 81 | + Amounts amounts{ |
| 82 | + {miner(0x01), 100}, |
| 83 | + {miner(0x02), 250}, |
| 84 | + {miner(0x03), 75}, |
| 85 | + {combined_script(), 40}, // donation weight keyed by COMBINED P2SH |
| 86 | + }; |
| 87 | + |
| 88 | + auto r = run_fold(amounts, subsidy); |
| 89 | + |
| 90 | + // per-miner dests exclude the donation script |
| 91 | + EXPECT_EQ(r.payout_outputs.size(), 3u); |
| 92 | + EXPECT_FALSE(contains_script(r.payout_outputs, combined_script())); |
| 93 | + EXPECT_FALSE(contains_script(r.payout_outputs, p2pk_script())); |
| 94 | + EXPECT_TRUE(contains_script(r.payout_outputs, miner(0x01))); |
| 95 | + EXPECT_TRUE(contains_script(r.payout_outputs, miner(0x02))); |
| 96 | + EXPECT_TRUE(contains_script(r.payout_outputs, miner(0x03))); |
| 97 | + |
| 98 | + // known answer: donation-last output grew by exactly the COMBINED weight |
| 99 | + // donation_initial = 1000 - (100+250+75+40) = 535 ; +40 folded = 575 |
| 100 | + EXPECT_EQ(r.donation_amount, 575u); |
| 101 | + EXPECT_EQ(sum_outputs(r.payout_outputs), 425u); |
| 102 | + |
| 103 | + // VALUE INVARIANCE: per-miner outputs + donation-last == subsidy |
| 104 | + EXPECT_EQ(sum_outputs(r.payout_outputs) + r.donation_amount, subsidy); |
| 105 | +} |
| 106 | + |
| 107 | +// Both donation scripts present; P2PK at canonical weight 0. Both excluded from |
| 108 | +// per-miner dests; dropping the 0-weight P2PK key is value-neutral. |
| 109 | +TEST(BTC_F11_DonationInvariance, BothDonationScriptsExcluded_P2PKZeroWeight) { |
| 110 | + const uint64_t subsidy = 5000; |
| 111 | + Amounts amounts{ |
| 112 | + {miner(0x0a), 1200}, |
| 113 | + {miner(0x0b), 800}, |
| 114 | + {combined_script(), 333}, |
| 115 | + {p2pk_script(), 0}, // canonical: P2PK never accrues weight |
| 116 | + }; |
| 117 | + |
| 118 | + auto r = run_fold(amounts, subsidy); |
| 119 | + |
| 120 | + EXPECT_EQ(r.payout_outputs.size(), 2u); |
| 121 | + EXPECT_FALSE(contains_script(r.payout_outputs, combined_script())); |
| 122 | + EXPECT_FALSE(contains_script(r.payout_outputs, p2pk_script())); |
| 123 | + |
| 124 | + // donation_initial = 5000 - (1200+800+333+0) = 2667 ; +333 = 3000 |
| 125 | + EXPECT_EQ(r.donation_amount, 3000u); |
| 126 | + EXPECT_EQ(sum_outputs(r.payout_outputs) + r.donation_amount, subsidy); |
| 127 | +} |
| 128 | + |
| 129 | +// No donation keys at all: the fold is an identity over the miner set and the |
| 130 | +// donation-last output is unchanged. |
| 131 | +TEST(BTC_F11_DonationInvariance, NoDonationKeys_Identity) { |
| 132 | + const uint64_t subsidy = 2000; |
| 133 | + Amounts amounts{ |
| 134 | + {miner(0x21), 600}, |
| 135 | + {miner(0x22), 400}, |
| 136 | + }; |
| 137 | + |
| 138 | + auto r = run_fold(amounts, subsidy); |
| 139 | + |
| 140 | + EXPECT_EQ(r.payout_outputs.size(), 2u); |
| 141 | + EXPECT_EQ(r.donation_amount, 1000u); // 2000 - 1000, no fold |
| 142 | + EXPECT_EQ(sum_outputs(r.payout_outputs) + r.donation_amount, subsidy); |
| 143 | +} |
| 144 | + |
| 145 | +// Boundary documentation: if the P2PK key DID carry weight (non-canonical), |
| 146 | +// dropping it would destroy exactly that many satoshis. This locks the reason |
| 147 | +// the canonical invariant requires P2PK weight == 0, so a future change that |
| 148 | +// starts keying weight on the P2PK script cannot pass silently. |
| 149 | +TEST(BTC_F11_DonationInvariance, DroppedP2PKWeightLeaksExactlyThatWeight) { |
| 150 | + const uint64_t subsidy = 1000; |
| 151 | + const uint64_t p2pk_weight = 30; // NON-canonical, for the boundary proof |
| 152 | + Amounts amounts{ |
| 153 | + {miner(0x31), 200}, |
| 154 | + {combined_script(), 50}, |
| 155 | + {p2pk_script(), p2pk_weight}, |
| 156 | + }; |
| 157 | + |
| 158 | + auto r = run_fold(amounts, subsidy); |
| 159 | + |
| 160 | + // COMBINED still folds correctly; P2PK weight is dropped (not folded). |
| 161 | + EXPECT_FALSE(contains_script(r.payout_outputs, p2pk_script())); |
| 162 | + // Total is short by EXACTLY the dropped P2PK weight — value is conserved |
| 163 | + // iff p2pk_weight == 0. |
| 164 | + EXPECT_EQ(sum_outputs(r.payout_outputs) + r.donation_amount, subsidy - p2pk_weight); |
| 165 | +} |
| 166 | + |
| 167 | +// Pin the actual consensus donation bytes so a constant change trips this test. |
| 168 | +TEST(BTC_F11_DonationInvariance, RealDonationConstantsPinned) { |
| 169 | + auto c = combined_script(); |
| 170 | + ASSERT_EQ(c.size(), 23u); // P2SH: OP_HASH160 <20> OP_EQUAL |
| 171 | + EXPECT_EQ(c.front(), 0xa9); |
| 172 | + EXPECT_EQ(c[1], 0x14); |
| 173 | + EXPECT_EQ(c.back(), 0x87); |
| 174 | + |
| 175 | + auto p = p2pk_script(); |
| 176 | + ASSERT_EQ(p.size(), 67u); // P2PK: OP_PUSHBYTES_65 <65> OP_CHECKSIG |
| 177 | + EXPECT_EQ(p.front(), 0x41); |
| 178 | + EXPECT_EQ(p.back(), 0xac); |
| 179 | + |
| 180 | + EXPECT_NE(c, p); |
| 181 | +} |
0 commit comments