/root/bitcoin/src/test/fuzz/rbf.cpp
Line | Count | Source |
1 | | // Copyright (c) 2020-present The Bitcoin Core developers |
2 | | // Distributed under the MIT software license, see the accompanying |
3 | | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
4 | | |
5 | | #include <node/mempool_args.h> |
6 | | #include <policy/rbf.h> |
7 | | #include <primitives/transaction.h> |
8 | | #include <sync.h> |
9 | | #include <test/fuzz/FuzzedDataProvider.h> |
10 | | #include <test/fuzz/fuzz.h> |
11 | | #include <test/fuzz/util.h> |
12 | | #include <test/fuzz/util/mempool.h> |
13 | | #include <test/util/setup_common.h> |
14 | | #include <test/util/time.h> |
15 | | #include <test/util/txmempool.h> |
16 | | #include <txmempool.h> |
17 | | #include <util/check.h> |
18 | | #include <util/translation.h> |
19 | | |
20 | | #include <cstdint> |
21 | | #include <optional> |
22 | | #include <string> |
23 | | #include <vector> |
24 | | |
25 | | namespace { |
26 | | const BasicTestingSetup* g_setup; |
27 | | } // namespace |
28 | | |
29 | | const int NUM_ITERS = 10000; |
30 | | |
31 | | std::vector<COutPoint> g_outpoints; |
32 | | |
33 | | void initialize_rbf() |
34 | 0 | { |
35 | 0 | static const auto testing_setup = MakeNoLogFileContext<>(); |
36 | 0 | g_setup = testing_setup.get(); |
37 | 0 | } |
38 | | |
39 | | void initialize_package_rbf() |
40 | 0 | { |
41 | 0 | static const auto testing_setup = MakeNoLogFileContext<>(); |
42 | 0 | g_setup = testing_setup.get(); |
43 | | |
44 | | // Create a fixed set of unique "UTXOs" to source parents from |
45 | | // to avoid fuzzer giving circular references |
46 | 0 | for (int i = 0; i < NUM_ITERS; ++i) { Branch (46:21): [True: 0, False: 0]
|
47 | 0 | g_outpoints.emplace_back(); |
48 | 0 | g_outpoints.back().n = i; |
49 | 0 | } |
50 | |
|
51 | 0 | } |
52 | | |
53 | | FUZZ_TARGET(rbf, .init = initialize_rbf) |
54 | 0 | { |
55 | 0 | SeedRandomStateForTest(SeedRand::ZEROS); |
56 | 0 | FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); |
57 | 0 | FakeNodeClock clock{ConsumeTime(fuzzed_data_provider)}; |
58 | 0 | std::optional<CMutableTransaction> mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); |
59 | 0 | if (!mtx) { Branch (59:9): [True: 0, False: 0]
|
60 | 0 | return; |
61 | 0 | } |
62 | | |
63 | 0 | bilingual_str error; |
64 | 0 | CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error}; |
65 | 0 | Assert(error.empty()); |
66 | |
|
67 | 0 | LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), NUM_ITERS) |
68 | 0 | { |
69 | 0 | const std::optional<CMutableTransaction> another_mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); |
70 | 0 | if (!another_mtx) { Branch (70:13): [True: 0, False: 0]
|
71 | 0 | break; |
72 | 0 | } |
73 | 0 | const CTransaction another_tx{*another_mtx}; |
74 | 0 | if (fuzzed_data_provider.ConsumeBool() && !mtx->vin.empty()) { Branch (74:13): [True: 0, False: 0]
Branch (74:51): [True: 0, False: 0]
|
75 | 0 | mtx->vin[0].prevout = COutPoint{another_tx.GetHash(), 0}; |
76 | 0 | } |
77 | 0 | LOCK2(cs_main, pool.cs); |
78 | 0 | if (!pool.GetIter(another_tx.GetHash())) { Branch (78:13): [True: 0, False: 0]
|
79 | 0 | TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, another_tx)); |
80 | 0 | } |
81 | 0 | } |
82 | 0 | const CTransaction tx{*mtx}; |
83 | 0 | if (fuzzed_data_provider.ConsumeBool()) { Branch (83:9): [True: 0, False: 0]
|
84 | 0 | LOCK2(cs_main, pool.cs); |
85 | 0 | if (!pool.GetIter(tx.GetHash())) { Branch (85:13): [True: 0, False: 0]
|
86 | 0 | TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, tx)); |
87 | 0 | } |
88 | 0 | } |
89 | 0 | { |
90 | 0 | LOCK(pool.cs); |
91 | 0 | (void)IsRBFOptIn(tx, pool); |
92 | 0 | } |
93 | 0 | } |
94 | | |
95 | | FUZZ_TARGET(package_rbf, .init = initialize_package_rbf) |
96 | 0 | { |
97 | 0 | SeedRandomStateForTest(SeedRand::ZEROS); |
98 | 0 | FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); |
99 | 0 | FakeNodeClock clock{ConsumeTime(fuzzed_data_provider)}; |
100 | | |
101 | | // "Real" virtual size is not important for this test since ConsumeTxMemPoolEntry generates its own virtual size values |
102 | | // so we construct small transactions for performance reasons. Child simply needs an input for later to perhaps connect to parent. |
103 | 0 | CMutableTransaction child; |
104 | 0 | child.vin.resize(1); |
105 | |
|
106 | 0 | bilingual_str error; |
107 | 0 | CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error}; |
108 | 0 | Assert(error.empty()); |
109 | | |
110 | | // Add a bunch of parent-child pairs to the mempool, and remember them. |
111 | 0 | std::vector<CTransaction> mempool_txs; |
112 | 0 | uint32_t iter{0}; |
113 | | |
114 | | // Keep track of the total vsize of CTxMemPoolEntry's being added to the mempool to avoid overflow |
115 | | // Add replacement_vsize since this is added to new diagram during RBF check |
116 | 0 | std::optional<CMutableTransaction> replacement_tx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); |
117 | 0 | if (!replacement_tx) { Branch (117:9): [True: 0, False: 0]
|
118 | 0 | return; |
119 | 0 | } |
120 | 0 | replacement_tx->vin.resize(1); |
121 | 0 | replacement_tx->vin[0].prevout = g_outpoints.at(iter++); |
122 | 0 | CTransaction replacement_tx_final{*replacement_tx}; |
123 | 0 | auto replacement_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, replacement_tx_final); |
124 | 0 | int32_t replacement_weight = replacement_entry.GetAdjustedWeight(); |
125 | | // Ensure that we don't hit FeeFrac limits, as we store TxGraph entries in terms of FeePerWeight |
126 | 0 | int64_t running_vsize_total{replacement_entry.GetTxSize()}; |
127 | |
|
128 | 0 | LOCK2(cs_main, pool.cs); |
129 | |
|
130 | 0 | while (fuzzed_data_provider.ConsumeBool()) { Branch (130:12): [True: 0, False: 0]
|
131 | 0 | if (iter >= NUM_ITERS) break; Branch (131:13): [True: 0, False: 0]
|
132 | | |
133 | | // Make sure txns only have one input, and that a unique input is given to avoid circular references |
134 | 0 | CMutableTransaction parent; |
135 | 0 | parent.vin.resize(1); |
136 | 0 | parent.vin[0].prevout = g_outpoints.at(iter++); |
137 | 0 | parent.vout.emplace_back(0, CScript()); |
138 | |
|
139 | 0 | mempool_txs.emplace_back(parent); |
140 | 0 | const auto parent_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); |
141 | 0 | running_vsize_total += parent_entry.GetTxSize(); |
142 | 0 | if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) { Branch (142:13): [True: 0, False: 0]
|
143 | | // We aren't adding this final tx to mempool, so we don't want to conflict with it |
144 | 0 | mempool_txs.pop_back(); |
145 | 0 | break; |
146 | 0 | } |
147 | 0 | assert(!pool.GetIter(parent_entry.GetTx().GetHash())); Branch (147:9): [True: 0, False: 0]
|
148 | 0 | TryAddToMempool(pool, parent_entry); |
149 | | |
150 | | // It's possible that adding this to the mempool failed due to cluster |
151 | | // size limits; if so bail out. |
152 | 0 | if(!pool.GetIter(parent_entry.GetTx().GetHash())) { Branch (152:12): [True: 0, False: 0]
|
153 | 0 | mempool_txs.pop_back(); |
154 | 0 | continue; |
155 | 0 | } |
156 | | |
157 | 0 | child.vin[0].prevout = COutPoint{mempool_txs.back().GetHash(), 0}; |
158 | 0 | mempool_txs.emplace_back(child); |
159 | 0 | const auto child_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); |
160 | 0 | running_vsize_total += child_entry.GetTxSize(); |
161 | 0 | if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) { Branch (161:13): [True: 0, False: 0]
|
162 | | // We aren't adding this final tx to mempool, so we don't want to conflict with it |
163 | 0 | mempool_txs.pop_back(); |
164 | 0 | break; |
165 | 0 | } |
166 | 0 | if (!pool.GetIter(child_entry.GetTx().GetHash())) { Branch (166:13): [True: 0, False: 0]
|
167 | 0 | TryAddToMempool(pool, child_entry); |
168 | | // Adding this transaction to the mempool may fail due to cluster |
169 | | // size limits; if so bail out. |
170 | 0 | if(!pool.GetIter(child_entry.GetTx().GetHash())) { Branch (170:16): [True: 0, False: 0]
|
171 | 0 | mempool_txs.pop_back(); |
172 | 0 | continue; |
173 | 0 | } |
174 | 0 | } |
175 | | |
176 | 0 | if (fuzzed_data_provider.ConsumeBool()) { Branch (176:13): [True: 0, False: 0]
|
177 | 0 | pool.PrioritiseTransaction(mempool_txs.back().GetHash(), fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(-100000, 100000)); |
178 | 0 | } |
179 | 0 | } |
180 | | |
181 | | // Pick some transactions at random to be the direct conflicts |
182 | 0 | CTxMemPool::setEntries direct_conflicts; |
183 | 0 | for (auto& tx : mempool_txs) { Branch (183:19): [True: 0, False: 0]
|
184 | 0 | if (fuzzed_data_provider.ConsumeBool() && pool.GetIter(tx.GetHash())) { Branch (184:13): [True: 0, False: 0]
Branch (184:13): [True: 0, False: 0]
Branch (184:51): [True: 0, False: 0]
|
185 | 0 | direct_conflicts.insert(*pool.GetIter(tx.GetHash())); |
186 | 0 | } |
187 | 0 | } |
188 | | |
189 | | // Calculate all conflicts: |
190 | 0 | CTxMemPool::setEntries all_conflicts; |
191 | 0 | for (auto& txiter : direct_conflicts) { Branch (191:23): [True: 0, False: 0]
|
192 | 0 | pool.CalculateDescendants(txiter, all_conflicts); |
193 | 0 | } |
194 | |
|
195 | 0 | CAmount replacement_fees = ConsumeMoney(fuzzed_data_provider); |
196 | 0 | auto changeset = pool.GetChangeSet(); |
197 | 0 | for (auto& txiter : all_conflicts) { Branch (197:23): [True: 0, False: 0]
|
198 | 0 | changeset->StageRemoval(txiter); |
199 | 0 | } |
200 | 0 | changeset->StageAddition(replacement_entry.GetSharedTx(), replacement_fees, |
201 | 0 | replacement_entry.GetTime().count(), replacement_entry.GetHeight(), |
202 | 0 | replacement_entry.GetSequence(), replacement_entry.GetSpendsCoinbase(), |
203 | 0 | replacement_entry.GetSigOpCost(), replacement_entry.GetLockPoints()); |
204 | | // Calculate the chunks for a replacement. |
205 | 0 | auto calc_results{changeset->CalculateChunksForRBF()}; |
206 | |
|
207 | 0 | if (calc_results.has_value()) { Branch (207:9): [True: 0, False: 0]
|
208 | | // Sanity checks on the chunks. |
209 | | |
210 | | // Feerates are monotonically decreasing. |
211 | 0 | FeeFrac first_sum; |
212 | 0 | for (size_t i = 0; i < calc_results->first.size(); ++i) { Branch (212:28): [True: 0, False: 0]
|
213 | 0 | first_sum += calc_results->first[i]; |
214 | 0 | if (i) assert(ByRatio{calc_results->first[i - 1]} >= ByRatio{calc_results->first[i]}); Branch (214:17): [True: 0, False: 0]
Branch (214:20): [True: 0, False: 0]
|
215 | 0 | } |
216 | 0 | FeeFrac second_sum; |
217 | 0 | for (size_t i = 0; i < calc_results->second.size(); ++i) { Branch (217:28): [True: 0, False: 0]
|
218 | 0 | second_sum += calc_results->second[i]; |
219 | 0 | if (i) assert(ByRatio{calc_results->second[i - 1]} >= ByRatio{calc_results->second[i]}); Branch (219:17): [True: 0, False: 0]
Branch (219:20): [True: 0, False: 0]
|
220 | 0 | } |
221 | | |
222 | 0 | FeeFrac replaced; |
223 | 0 | for (auto txiter : all_conflicts) { Branch (223:26): [True: 0, False: 0]
|
224 | 0 | replaced.fee += txiter->GetModifiedFee(); |
225 | 0 | replaced.size += txiter->GetAdjustedWeight(); |
226 | 0 | } |
227 | | // The total fee & size of the new diagram minus replaced fee & size should be the total |
228 | | // fee & size of the old diagram minus replacement fee & size. |
229 | 0 | assert((first_sum - replaced) == (second_sum - FeeFrac{replacement_fees, replacement_weight})); Branch (229:9): [True: 0, False: 0]
|
230 | 0 | } |
231 | | |
232 | | // If internals report error, wrapper should too |
233 | 0 | auto err_tuple{ImprovesFeerateDiagram(*changeset)}; |
234 | 0 | if (!calc_results.has_value()) { Branch (234:9): [True: 0, False: 0]
|
235 | 0 | assert(err_tuple.value().first == DiagramCheckError::UNCALCULABLE); Branch (235:10): [True: 0, False: 0]
|
236 | 0 | } else { |
237 | | // Diagram check succeeded |
238 | 0 | auto old_sum = std::accumulate(calc_results->first.begin(), calc_results->first.end(), FeeFrac{}); |
239 | 0 | auto new_sum = std::accumulate(calc_results->second.begin(), calc_results->second.end(), FeeFrac{}); |
240 | 0 | if (!err_tuple.has_value()) { Branch (240:13): [True: 0, False: 0]
|
241 | | // New diagram's final fee should always match or exceed old diagram's |
242 | 0 | assert(old_sum.fee <= new_sum.fee); Branch (242:13): [True: 0, False: 0]
|
243 | 0 | } else if (old_sum.fee > new_sum.fee) { Branch (243:20): [True: 0, False: 0]
|
244 | | // Or it failed, and if old diagram had higher fees, it should be a failure |
245 | | assert(err_tuple.value().first == DiagramCheckError::FAILURE); Branch (245:13): [True: 0, False: 0]
|
246 | 0 | } |
247 | 0 | } |
248 | 0 | } |