/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 | const std::optional<CMutableTransaction> another_mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); |
69 | 0 | if (!another_mtx) { Branch (69:13): [True: 0, False: 0]
|
70 | 0 | break; |
71 | 0 | } |
72 | 0 | const CTransaction another_tx{*another_mtx}; |
73 | 0 | if (fuzzed_data_provider.ConsumeBool() && !mtx->vin.empty()) { Branch (73:13): [True: 0, False: 0]
Branch (73:51): [True: 0, False: 0]
|
74 | 0 | mtx->vin[0].prevout = COutPoint{another_tx.GetHash(), 0}; |
75 | 0 | } |
76 | 0 | LOCK2(cs_main, pool.cs); |
77 | 0 | if (!pool.GetIter(another_tx.GetHash())) { Branch (77:13): [True: 0, False: 0]
|
78 | 0 | TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, another_tx)); |
79 | 0 | } |
80 | 0 | } |
81 | 0 | const CTransaction tx{*mtx}; |
82 | 0 | if (fuzzed_data_provider.ConsumeBool()) { Branch (82:9): [True: 0, False: 0]
|
83 | 0 | LOCK2(cs_main, pool.cs); |
84 | 0 | if (!pool.GetIter(tx.GetHash())) { Branch (84:13): [True: 0, False: 0]
|
85 | 0 | TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, tx)); |
86 | 0 | } |
87 | 0 | } |
88 | 0 | { |
89 | 0 | LOCK(pool.cs); |
90 | 0 | (void)IsRBFOptIn(tx, pool); |
91 | 0 | } |
92 | 0 | } |
93 | | |
94 | | FUZZ_TARGET(package_rbf, .init = initialize_package_rbf) |
95 | 0 | { |
96 | 0 | SeedRandomStateForTest(SeedRand::ZEROS); |
97 | 0 | FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); |
98 | 0 | FakeNodeClock clock{ConsumeTime(fuzzed_data_provider)}; |
99 | | |
100 | | // "Real" virtual size is not important for this test since ConsumeTxMemPoolEntry generates its own virtual size values |
101 | | // so we construct small transactions for performance reasons. Child simply needs an input for later to perhaps connect to parent. |
102 | 0 | CMutableTransaction child; |
103 | 0 | child.vin.resize(1); |
104 | |
|
105 | 0 | bilingual_str error; |
106 | 0 | CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error}; |
107 | 0 | Assert(error.empty()); |
108 | | |
109 | | // Add a bunch of parent-child pairs to the mempool, and remember them. |
110 | 0 | std::vector<CTransaction> mempool_txs; |
111 | 0 | uint32_t iter{0}; |
112 | | |
113 | | // Keep track of the total vsize of CTxMemPoolEntry's being added to the mempool to avoid overflow |
114 | | // Add replacement_vsize since this is added to new diagram during RBF check |
115 | 0 | std::optional<CMutableTransaction> replacement_tx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); |
116 | 0 | if (!replacement_tx) { Branch (116:9): [True: 0, False: 0]
|
117 | 0 | return; |
118 | 0 | } |
119 | 0 | replacement_tx->vin.resize(1); |
120 | 0 | replacement_tx->vin[0].prevout = g_outpoints.at(iter++); |
121 | 0 | CTransaction replacement_tx_final{*replacement_tx}; |
122 | 0 | auto replacement_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, replacement_tx_final); |
123 | 0 | int32_t replacement_weight = replacement_entry.GetAdjustedWeight(); |
124 | | // Ensure that we don't hit FeeFrac limits, as we store TxGraph entries in terms of FeePerWeight |
125 | 0 | int64_t running_vsize_total{replacement_entry.GetTxSize()}; |
126 | |
|
127 | 0 | LOCK2(cs_main, pool.cs); |
128 | |
|
129 | 0 | while (fuzzed_data_provider.ConsumeBool()) { Branch (129:12): [True: 0, False: 0]
|
130 | 0 | if (iter >= NUM_ITERS) break; Branch (130:13): [True: 0, False: 0]
|
131 | | |
132 | | // Make sure txns only have one input, and that a unique input is given to avoid circular references |
133 | 0 | CMutableTransaction parent; |
134 | 0 | parent.vin.resize(1); |
135 | 0 | parent.vin[0].prevout = g_outpoints.at(iter++); |
136 | 0 | parent.vout.emplace_back(0, CScript()); |
137 | |
|
138 | 0 | mempool_txs.emplace_back(parent); |
139 | 0 | const auto parent_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); |
140 | 0 | running_vsize_total += parent_entry.GetTxSize(); |
141 | 0 | if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) { Branch (141:13): [True: 0, False: 0]
|
142 | | // We aren't adding this final tx to mempool, so we don't want to conflict with it |
143 | 0 | mempool_txs.pop_back(); |
144 | 0 | break; |
145 | 0 | } |
146 | 0 | assert(!pool.GetIter(parent_entry.GetTx().GetHash())); Branch (146:9): [True: 0, False: 0]
|
147 | 0 | TryAddToMempool(pool, parent_entry); |
148 | | |
149 | | // It's possible that adding this to the mempool failed due to cluster |
150 | | // size limits; if so bail out. |
151 | 0 | if(!pool.GetIter(parent_entry.GetTx().GetHash())) { Branch (151:12): [True: 0, False: 0]
|
152 | 0 | mempool_txs.pop_back(); |
153 | 0 | continue; |
154 | 0 | } |
155 | | |
156 | 0 | child.vin[0].prevout = COutPoint{mempool_txs.back().GetHash(), 0}; |
157 | 0 | mempool_txs.emplace_back(child); |
158 | 0 | const auto child_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); |
159 | 0 | running_vsize_total += child_entry.GetTxSize(); |
160 | 0 | if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) { Branch (160:13): [True: 0, False: 0]
|
161 | | // We aren't adding this final tx to mempool, so we don't want to conflict with it |
162 | 0 | mempool_txs.pop_back(); |
163 | 0 | break; |
164 | 0 | } |
165 | 0 | if (!pool.GetIter(child_entry.GetTx().GetHash())) { Branch (165:13): [True: 0, False: 0]
|
166 | 0 | TryAddToMempool(pool, child_entry); |
167 | | // Adding this transaction to the mempool may fail due to cluster |
168 | | // size limits; if so bail out. |
169 | 0 | if(!pool.GetIter(child_entry.GetTx().GetHash())) { Branch (169:16): [True: 0, False: 0]
|
170 | 0 | mempool_txs.pop_back(); |
171 | 0 | continue; |
172 | 0 | } |
173 | 0 | } |
174 | | |
175 | 0 | if (fuzzed_data_provider.ConsumeBool()) { Branch (175:13): [True: 0, False: 0]
|
176 | 0 | pool.PrioritiseTransaction(mempool_txs.back().GetHash(), fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(-100000, 100000)); |
177 | 0 | } |
178 | 0 | } |
179 | | |
180 | | // Pick some transactions at random to be the direct conflicts |
181 | 0 | CTxMemPool::setEntries direct_conflicts; |
182 | 0 | for (auto& tx : mempool_txs) { Branch (182:19): [True: 0, False: 0]
|
183 | 0 | if (fuzzed_data_provider.ConsumeBool() && pool.GetIter(tx.GetHash())) { Branch (183:13): [True: 0, False: 0]
Branch (183:13): [True: 0, False: 0]
Branch (183:51): [True: 0, False: 0]
|
184 | 0 | direct_conflicts.insert(*pool.GetIter(tx.GetHash())); |
185 | 0 | } |
186 | 0 | } |
187 | | |
188 | | // Calculate all conflicts: |
189 | 0 | CTxMemPool::setEntries all_conflicts; |
190 | 0 | for (auto& txiter : direct_conflicts) { Branch (190:23): [True: 0, False: 0]
|
191 | 0 | pool.CalculateDescendants(txiter, all_conflicts); |
192 | 0 | } |
193 | |
|
194 | 0 | CAmount replacement_fees = ConsumeMoney(fuzzed_data_provider); |
195 | 0 | auto changeset = pool.GetChangeSet(); |
196 | 0 | for (auto& txiter : all_conflicts) { Branch (196:23): [True: 0, False: 0]
|
197 | 0 | changeset->StageRemoval(txiter); |
198 | 0 | } |
199 | 0 | changeset->StageAddition(replacement_entry.GetSharedTx(), replacement_fees, |
200 | 0 | replacement_entry.GetTime().count(), replacement_entry.GetHeight(), |
201 | 0 | replacement_entry.GetSequence(), replacement_entry.GetSpendsCoinbase(), |
202 | 0 | replacement_entry.GetSigOpCost(), replacement_entry.GetLockPoints()); |
203 | | // Calculate the chunks for a replacement. |
204 | 0 | auto calc_results{changeset->CalculateChunksForRBF()}; |
205 | |
|
206 | 0 | if (calc_results.has_value()) { Branch (206:9): [True: 0, False: 0]
|
207 | | // Sanity checks on the chunks. |
208 | | |
209 | | // Feerates are monotonically decreasing. |
210 | 0 | FeeFrac first_sum; |
211 | 0 | for (size_t i = 0; i < calc_results->first.size(); ++i) { Branch (211:28): [True: 0, False: 0]
|
212 | 0 | first_sum += calc_results->first[i]; |
213 | 0 | if (i) assert(ByRatio{calc_results->first[i - 1]} >= ByRatio{calc_results->first[i]}); Branch (213:17): [True: 0, False: 0]
Branch (213:20): [True: 0, False: 0]
|
214 | 0 | } |
215 | 0 | FeeFrac second_sum; |
216 | 0 | for (size_t i = 0; i < calc_results->second.size(); ++i) { Branch (216:28): [True: 0, False: 0]
|
217 | 0 | second_sum += calc_results->second[i]; |
218 | 0 | if (i) assert(ByRatio{calc_results->second[i - 1]} >= ByRatio{calc_results->second[i]}); Branch (218:17): [True: 0, False: 0]
Branch (218:20): [True: 0, False: 0]
|
219 | 0 | } |
220 | | |
221 | 0 | FeeFrac replaced; |
222 | 0 | for (auto txiter : all_conflicts) { Branch (222:26): [True: 0, False: 0]
|
223 | 0 | replaced.fee += txiter->GetModifiedFee(); |
224 | 0 | replaced.size += txiter->GetAdjustedWeight(); |
225 | 0 | } |
226 | | // The total fee & size of the new diagram minus replaced fee & size should be the total |
227 | | // fee & size of the old diagram minus replacement fee & size. |
228 | 0 | assert((first_sum - replaced) == (second_sum - FeeFrac{replacement_fees, replacement_weight})); Branch (228:9): [True: 0, False: 0]
|
229 | 0 | } |
230 | | |
231 | | // If internals report error, wrapper should too |
232 | 0 | auto err_tuple{ImprovesFeerateDiagram(*changeset)}; |
233 | 0 | if (!calc_results.has_value()) { Branch (233:9): [True: 0, False: 0]
|
234 | 0 | assert(err_tuple.value().first == DiagramCheckError::UNCALCULABLE); Branch (234:10): [True: 0, False: 0]
|
235 | 0 | } else { |
236 | | // Diagram check succeeded |
237 | 0 | auto old_sum = std::accumulate(calc_results->first.begin(), calc_results->first.end(), FeeFrac{}); |
238 | 0 | auto new_sum = std::accumulate(calc_results->second.begin(), calc_results->second.end(), FeeFrac{}); |
239 | 0 | if (!err_tuple.has_value()) { Branch (239:13): [True: 0, False: 0]
|
240 | | // New diagram's final fee should always match or exceed old diagram's |
241 | 0 | assert(old_sum.fee <= new_sum.fee); Branch (241:13): [True: 0, False: 0]
|
242 | 0 | } else if (old_sum.fee > new_sum.fee) { Branch (242:20): [True: 0, False: 0]
|
243 | | // Or it failed, and if old diagram had higher fees, it should be a failure |
244 | | assert(err_tuple.value().first == DiagramCheckError::FAILURE); Branch (244:13): [True: 0, False: 0]
|
245 | 0 | } |
246 | 0 | } |
247 | 0 | } |