/root/bitcoin/src/test/fuzz/cluster_linearize.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright (c) 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 <cluster_linearize.h> |
6 | | #include <random.h> |
7 | | #include <serialize.h> |
8 | | #include <streams.h> |
9 | | #include <test/fuzz/FuzzedDataProvider.h> |
10 | | #include <test/fuzz/fuzz.h> |
11 | | #include <test/util/cluster_linearize.h> |
12 | | #include <util/bitset.h> |
13 | | #include <util/feefrac.h> |
14 | | |
15 | | #include <algorithm> |
16 | | #include <cstdint> |
17 | | #include <utility> |
18 | | #include <vector> |
19 | | |
20 | | /* |
21 | | * The tests in this file primarily cover the candidate finder classes and linearization algorithms. |
22 | | * |
23 | | * <----: An implementation (at the start of the line --) is tested in the test marked with *, |
24 | | * possibly by comparison with other implementations (at the end of the line ->). |
25 | | * <<---: The right side is implemented using the left side. |
26 | | * |
27 | | * +-----------------------+ |
28 | | * | SearchCandidateFinder | <<---------------------\ |
29 | | * +-----------------------+ | |
30 | | * | +-----------+ |
31 | | * | | Linearize | |
32 | | * | +-----------+ |
33 | | * | +-------------------------+ | | |
34 | | * | | AncestorCandidateFinder | <<--------/ | |
35 | | * | +-------------------------+ | |
36 | | * | | ^ | ^^ PRODUCTION CODE |
37 | | * | | | | || |
38 | | * ============================================================================================== |
39 | | * | | | | || |
40 | | * | clusterlin_ancestor_finder* | | vv TEST CODE |
41 | | * | | | |
42 | | * |-clusterlin_search_finder* | |-clusterlin_linearize* |
43 | | * | | | |
44 | | * v | v |
45 | | * +-----------------------+ | +-----------------+ |
46 | | * | SimpleCandidateFinder | <<-------------------| SimpleLinearize | |
47 | | * +-----------------------+ | +-----------------+ |
48 | | * | | | |
49 | | * +-------------------/ | |
50 | | * | | |
51 | | * |-clusterlin_simple_finder* |-clusterlin_simple_linearize* |
52 | | * v v |
53 | | * +---------------------------+ +---------------------+ |
54 | | * | ExhaustiveCandidateFinder | | ExhaustiveLinearize | |
55 | | * +---------------------------+ +---------------------+ |
56 | | * |
57 | | * More tests are included for lower-level and related functions and classes: |
58 | | * - DepGraph tests: |
59 | | * - clusterlin_depgraph_sim |
60 | | * - clusterlin_depgraph_serialization |
61 | | * - clusterlin_components |
62 | | * - ChunkLinearization and LinearizationChunking tests: |
63 | | * - clusterlin_chunking |
64 | | * - clusterlin_linearization_chunking |
65 | | * - PostLinearize tests: |
66 | | * - clusterlin_postlinearize |
67 | | * - clusterlin_postlinearize_tree |
68 | | * - clusterlin_postlinearize_moved_leaf |
69 | | * - MergeLinearization tests: |
70 | | * - clusterlin_merge |
71 | | * - FixLinearization tests: |
72 | | * - clusterlin_fix_linearization |
73 | | * - MakeConnected tests (a test-only function): |
74 | | * - clusterlin_make_connected |
75 | | */ |
76 | | |
77 | | using namespace cluster_linearize; |
78 | | |
79 | | namespace { |
80 | | |
81 | | /** A simple finder class for candidate sets. |
82 | | * |
83 | | * This class matches SearchCandidateFinder in interface and behavior, though with fewer |
84 | | * optimizations. |
85 | | */ |
86 | | template<typename SetType> |
87 | | class SimpleCandidateFinder |
88 | | { |
89 | | /** Internal dependency graph. */ |
90 | | const DepGraph<SetType>& m_depgraph; |
91 | | /** Which transaction are left to include. */ |
92 | | SetType m_todo; |
93 | | |
94 | | public: |
95 | | /** Construct an SimpleCandidateFinder for a given graph. */ |
96 | | SimpleCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept : |
97 | 0 | m_depgraph(depgraph), m_todo{depgraph.Positions()} {} |
98 | | |
99 | | /** Remove a set of transactions from the set of to-be-linearized ones. */ |
100 | 0 | void MarkDone(SetType select) noexcept { m_todo -= select; } |
101 | | |
102 | | /** Determine whether unlinearized transactions remain. */ |
103 | 0 | bool AllDone() const noexcept { return m_todo.None(); } |
104 | | |
105 | | /** Find a candidate set using at most max_iterations iterations, and the number of iterations |
106 | | * actually performed. If that number is less than max_iterations, then the result is optimal. |
107 | | * |
108 | | * Always returns a connected set of transactions. |
109 | | * |
110 | | * Complexity: O(N * M), where M is the number of connected topological subsets of the cluster. |
111 | | * That number is bounded by M <= 2^(N-1). |
112 | | */ |
113 | | std::pair<SetInfo<SetType>, uint64_t> FindCandidateSet(uint64_t max_iterations) const noexcept |
114 | 0 | { |
115 | 0 | uint64_t iterations_left = max_iterations; |
116 | | // Queue of work units. Each consists of: |
117 | | // - inc: set of transactions definitely included |
118 | | // - und: set of transactions that can be added to inc still |
119 | 0 | std::vector<std::pair<SetType, SetType>> queue; |
120 | | // Initially we have just one queue element, with the entire graph in und. |
121 | 0 | queue.emplace_back(SetType{}, m_todo); |
122 | | // Best solution so far. Initialize with the remaining ancestors of the first remaining |
123 | | // transaction. |
124 | 0 | SetInfo best(m_depgraph, m_depgraph.Ancestors(m_todo.First()) & m_todo); |
125 | | // Process the queue. |
126 | 0 | while (!queue.empty() && iterations_left) { |
127 | | // Pop top element of the queue. |
128 | 0 | auto [inc, und] = queue.back(); |
129 | 0 | queue.pop_back(); |
130 | | // Look for a transaction to consider adding/removing. |
131 | 0 | bool inc_none = inc.None(); |
132 | 0 | for (auto split : und) { |
133 | | // If inc is empty, consider any split transaction. Otherwise only consider |
134 | | // transactions that share ancestry with inc so far (which means only connected |
135 | | // sets will be considered). |
136 | 0 | if (inc_none || inc.Overlaps(m_depgraph.Ancestors(split))) { |
137 | 0 | --iterations_left; |
138 | | // Add a queue entry with split included. |
139 | 0 | SetInfo new_inc(m_depgraph, inc | (m_todo & m_depgraph.Ancestors(split))); |
140 | 0 | queue.emplace_back(new_inc.transactions, und - new_inc.transactions); |
141 | | // Add a queue entry with split excluded. |
142 | 0 | queue.emplace_back(inc, und - m_depgraph.Descendants(split)); |
143 | | // Update statistics to account for the candidate new_inc. |
144 | 0 | if (new_inc.feerate > best.feerate) best = new_inc; |
145 | 0 | break; |
146 | 0 | } |
147 | 0 | } |
148 | 0 | } |
149 | 0 | return {std::move(best), max_iterations - iterations_left}; |
150 | 0 | } |
151 | | }; |
152 | | |
153 | | /** A very simple finder class for optimal candidate sets, which tries every subset. |
154 | | * |
155 | | * It is even simpler than SimpleCandidateFinder, and exists just to help test the correctness of |
156 | | * SimpleCandidateFinder, which is then used to test the correctness of SearchCandidateFinder. |
157 | | */ |
158 | | template<typename SetType> |
159 | | class ExhaustiveCandidateFinder |
160 | | { |
161 | | /** Internal dependency graph. */ |
162 | | const DepGraph<SetType>& m_depgraph; |
163 | | /** Which transaction are left to include. */ |
164 | | SetType m_todo; |
165 | | |
166 | | public: |
167 | | /** Construct an ExhaustiveCandidateFinder for a given graph. */ |
168 | | ExhaustiveCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept : |
169 | 0 | m_depgraph(depgraph), m_todo{depgraph.Positions()} {} |
170 | | |
171 | | /** Remove a set of transactions from the set of to-be-linearized ones. */ |
172 | 0 | void MarkDone(SetType select) noexcept { m_todo -= select; } |
173 | | |
174 | | /** Determine whether unlinearized transactions remain. */ |
175 | 0 | bool AllDone() const noexcept { return m_todo.None(); } |
176 | | |
177 | | /** Find the optimal remaining candidate set. |
178 | | * |
179 | | * Complexity: O(N * 2^N). |
180 | | */ |
181 | | SetInfo<SetType> FindCandidateSet() const noexcept |
182 | 0 | { |
183 | | // Best solution so far. |
184 | 0 | SetInfo<SetType> best{m_todo, m_depgraph.FeeRate(m_todo)}; |
185 | | // The number of combinations to try. |
186 | 0 | uint64_t limit = (uint64_t{1} << m_todo.Count()) - 1; |
187 | | // Try the transitive closure of every non-empty subset of m_todo. |
188 | 0 | for (uint64_t x = 1; x < limit; ++x) { |
189 | | // If bit number b is set in x, then the remaining ancestors of the b'th remaining |
190 | | // transaction in m_todo are included. |
191 | 0 | SetType txn; |
192 | 0 | auto x_shifted{x}; |
193 | 0 | for (auto i : m_todo) { |
194 | 0 | if (x_shifted & 1) txn |= m_depgraph.Ancestors(i); |
195 | 0 | x_shifted >>= 1; |
196 | 0 | } |
197 | 0 | SetInfo cur(m_depgraph, txn & m_todo); |
198 | 0 | if (cur.feerate > best.feerate) best = cur; |
199 | 0 | } |
200 | 0 | return best; |
201 | 0 | } |
202 | | }; |
203 | | |
204 | | /** A simple linearization algorithm. |
205 | | * |
206 | | * This matches Linearize() in interface and behavior, though with fewer optimizations, lacking |
207 | | * the ability to pass in an existing linearization, and using just SimpleCandidateFinder rather |
208 | | * than AncestorCandidateFinder and SearchCandidateFinder. |
209 | | */ |
210 | | template<typename SetType> |
211 | | std::pair<std::vector<DepGraphIndex>, bool> SimpleLinearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations) |
212 | 0 | { |
213 | 0 | std::vector<DepGraphIndex> linearization; |
214 | 0 | SimpleCandidateFinder finder(depgraph); |
215 | 0 | SetType todo = depgraph.Positions(); |
216 | 0 | bool optimal = true; |
217 | 0 | while (todo.Any()) { |
218 | 0 | auto [candidate, iterations_done] = finder.FindCandidateSet(max_iterations); |
219 | 0 | if (iterations_done == max_iterations) optimal = false; |
220 | 0 | depgraph.AppendTopo(linearization, candidate.transactions); |
221 | 0 | todo -= candidate.transactions; |
222 | 0 | finder.MarkDone(candidate.transactions); |
223 | 0 | max_iterations -= iterations_done; |
224 | 0 | } |
225 | 0 | return {std::move(linearization), optimal}; |
226 | 0 | } |
227 | | |
228 | | /** An even simpler linearization algorithm that tries all permutations. |
229 | | * |
230 | | * This roughly matches SimpleLinearize() (and Linearize) in interface and behavior, but always |
231 | | * tries all topologically-valid transaction orderings, has no way to bound how much work it does, |
232 | | * and always finds the optimal. With an O(n!) complexity, it should only be used for small |
233 | | * clusters. |
234 | | */ |
235 | | template<typename SetType> |
236 | | std::vector<DepGraphIndex> ExhaustiveLinearize(const DepGraph<SetType>& depgraph) |
237 | 0 | { |
238 | | // The best linearization so far, and its chunking. |
239 | 0 | std::vector<DepGraphIndex> linearization; |
240 | 0 | std::vector<FeeFrac> chunking; |
241 | |
|
242 | 0 | std::vector<DepGraphIndex> perm_linearization; |
243 | | // Initialize with the lexicographically-first linearization. |
244 | 0 | for (DepGraphIndex i : depgraph.Positions()) perm_linearization.push_back(i); |
245 | | // Iterate over all valid permutations. |
246 | 0 | do { |
247 | | /** What prefix of perm_linearization is topological. */ |
248 | 0 | DepGraphIndex topo_length{0}; |
249 | 0 | TestBitSet perm_done; |
250 | 0 | while (topo_length < perm_linearization.size()) { |
251 | 0 | auto i = perm_linearization[topo_length]; |
252 | 0 | perm_done.Set(i); |
253 | 0 | if (!depgraph.Ancestors(i).IsSubsetOf(perm_done)) break; |
254 | 0 | ++topo_length; |
255 | 0 | } |
256 | 0 | if (topo_length == perm_linearization.size()) { |
257 | | // If all of perm_linearization is topological, check if it is perhaps our best |
258 | | // linearization so far. |
259 | 0 | auto perm_chunking = ChunkLinearization(depgraph, perm_linearization); |
260 | 0 | auto cmp = CompareChunks(perm_chunking, chunking); |
261 | | // If the diagram is better, or if it is equal but with more chunks (because we |
262 | | // prefer minimal chunks), consider this better. |
263 | 0 | if (linearization.empty() || cmp > 0 || (cmp == 0 && perm_chunking.size() > chunking.size())) { |
264 | 0 | linearization = perm_linearization; |
265 | 0 | chunking = perm_chunking; |
266 | 0 | } |
267 | 0 | } else { |
268 | | // Otherwise, fast forward to the last permutation with the same non-topological |
269 | | // prefix. |
270 | 0 | auto first_non_topo = perm_linearization.begin() + topo_length; |
271 | 0 | assert(std::is_sorted(first_non_topo + 1, perm_linearization.end())); |
272 | 0 | std::reverse(first_non_topo + 1, perm_linearization.end()); |
273 | 0 | } |
274 | 0 | } while(std::next_permutation(perm_linearization.begin(), perm_linearization.end())); |
275 | | |
276 | 0 | return linearization; |
277 | 0 | } |
278 | | |
279 | | |
280 | | /** Stitch connected components together in a DepGraph, guaranteeing its corresponding cluster is connected. */ |
281 | | template<typename BS> |
282 | | void MakeConnected(DepGraph<BS>& depgraph) |
283 | 0 | { |
284 | 0 | auto todo = depgraph.Positions(); |
285 | 0 | auto comp = depgraph.FindConnectedComponent(todo); |
286 | 0 | Assume(depgraph.IsConnected(comp)); |
287 | 0 | todo -= comp; |
288 | 0 | while (todo.Any()) { |
289 | 0 | auto nextcomp = depgraph.FindConnectedComponent(todo); |
290 | 0 | Assume(depgraph.IsConnected(nextcomp)); |
291 | 0 | depgraph.AddDependencies(BS::Singleton(comp.Last()), nextcomp.First()); |
292 | 0 | todo -= nextcomp; |
293 | 0 | comp = nextcomp; |
294 | 0 | } |
295 | 0 | } |
296 | | |
297 | | /** Given a dependency graph, and a todo set, read a topological subset of todo from reader. */ |
298 | | template<typename SetType> |
299 | | SetType ReadTopologicalSet(const DepGraph<SetType>& depgraph, const SetType& todo, SpanReader& reader, bool non_empty) |
300 | 0 | { |
301 | | // Read a bitmask from the fuzzing input. Add 1 if non_empty, so the mask is definitely not |
302 | | // zero in that case. |
303 | 0 | uint64_t mask{0}; |
304 | 0 | try { |
305 | 0 | reader >> VARINT(mask); |
306 | 0 | } catch(const std::ios_base::failure&) {} |
307 | 0 | if (mask != uint64_t(-1)) mask += non_empty; |
308 | |
|
309 | 0 | SetType ret; |
310 | 0 | for (auto i : todo) { |
311 | 0 | if (!ret[i]) { |
312 | 0 | if (mask & 1) ret |= depgraph.Ancestors(i); |
313 | 0 | mask >>= 1; |
314 | 0 | } |
315 | 0 | } |
316 | 0 | ret &= todo; |
317 | | |
318 | | // While mask starts off non-zero if non_empty is true, it is still possible that all its low |
319 | | // bits are 0, and ret ends up being empty. As a last resort, use the in-todo ancestry of the |
320 | | // first todo position. |
321 | 0 | if (non_empty && ret.None()) { |
322 | 0 | Assume(todo.Any()); |
323 | 0 | ret = depgraph.Ancestors(todo.First()) & todo; |
324 | 0 | Assume(ret.Any()); |
325 | 0 | } |
326 | 0 | return ret; |
327 | 0 | } |
328 | | |
329 | | /** Given a dependency graph, construct any valid linearization for it, reading from a SpanReader. */ |
330 | | template<typename BS> |
331 | | std::vector<DepGraphIndex> ReadLinearization(const DepGraph<BS>& depgraph, SpanReader& reader) |
332 | 0 | { |
333 | 0 | std::vector<DepGraphIndex> linearization; |
334 | 0 | TestBitSet todo = depgraph.Positions(); |
335 | | // In every iteration one topologically-valid transaction is appended to linearization. |
336 | 0 | while (todo.Any()) { |
337 | | // Compute the set of transactions with no not-yet-included ancestors. |
338 | 0 | TestBitSet potential_next; |
339 | 0 | for (auto j : todo) { |
340 | 0 | if ((depgraph.Ancestors(j) & todo) == TestBitSet::Singleton(j)) { |
341 | 0 | potential_next.Set(j); |
342 | 0 | } |
343 | 0 | } |
344 | | // There must always be one (otherwise there is a cycle in the graph). |
345 | 0 | assert(potential_next.Any()); |
346 | | // Read a number from reader, and interpret it as index into potential_next. |
347 | 0 | uint64_t idx{0}; |
348 | 0 | try { |
349 | 0 | reader >> VARINT(idx); |
350 | 0 | } catch (const std::ios_base::failure&) {} |
351 | 0 | idx %= potential_next.Count(); |
352 | | // Find out which transaction that corresponds to. |
353 | 0 | for (auto j : potential_next) { |
354 | 0 | if (idx == 0) { |
355 | | // When found, add it to linearization and remove it from todo. |
356 | 0 | linearization.push_back(j); |
357 | 0 | assert(todo[j]); |
358 | 0 | todo.Reset(j); |
359 | 0 | break; |
360 | 0 | } |
361 | 0 | --idx; |
362 | 0 | } |
363 | 0 | } |
364 | 0 | return linearization; |
365 | 0 | } |
366 | | |
367 | | } // namespace |
368 | | |
369 | | FUZZ_TARGET(clusterlin_depgraph_sim) |
370 | 0 | { |
371 | | // Simulation test to verify the full behavior of DepGraph. |
372 | |
|
373 | 0 | FuzzedDataProvider provider(buffer.data(), buffer.size()); |
374 | | |
375 | | /** Real DepGraph being tested. */ |
376 | 0 | DepGraph<TestBitSet> real; |
377 | | /** Simulated DepGraph (sim[i] is std::nullopt if position i does not exist; otherwise, |
378 | | * sim[i]->first is its individual feerate, and sim[i]->second is its set of ancestors. */ |
379 | 0 | std::array<std::optional<std::pair<FeeFrac, TestBitSet>>, TestBitSet::Size()> sim; |
380 | | /** The number of non-nullopt position in sim. */ |
381 | 0 | DepGraphIndex num_tx_sim{0}; |
382 | | |
383 | | /** Read a valid index of a transaction from the provider. */ |
384 | 0 | auto idx_fn = [&]() { |
385 | 0 | auto offset = provider.ConsumeIntegralInRange<DepGraphIndex>(0, num_tx_sim - 1); |
386 | 0 | for (DepGraphIndex i = 0; i < sim.size(); ++i) { |
387 | 0 | if (!sim[i].has_value()) continue; |
388 | 0 | if (offset == 0) return i; |
389 | 0 | --offset; |
390 | 0 | } |
391 | 0 | assert(false); |
392 | 0 | return DepGraphIndex(-1); |
393 | 0 | }; |
394 | | |
395 | | /** Read a valid subset of the transactions from the provider. */ |
396 | 0 | auto subset_fn = [&]() { |
397 | 0 | auto range = (uint64_t{1} << num_tx_sim) - 1; |
398 | 0 | const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range); |
399 | 0 | auto mask_shifted = mask; |
400 | 0 | TestBitSet subset; |
401 | 0 | for (DepGraphIndex i = 0; i < sim.size(); ++i) { |
402 | 0 | if (!sim[i].has_value()) continue; |
403 | 0 | if (mask_shifted & 1) { |
404 | 0 | subset.Set(i); |
405 | 0 | } |
406 | 0 | mask_shifted >>= 1; |
407 | 0 | } |
408 | 0 | assert(mask_shifted == 0); |
409 | 0 | return subset; |
410 | 0 | }; |
411 | | |
412 | | /** Read any set of transactions from the provider (including unused positions). */ |
413 | 0 | auto set_fn = [&]() { |
414 | 0 | auto range = (uint64_t{1} << sim.size()) - 1; |
415 | 0 | const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range); |
416 | 0 | TestBitSet set; |
417 | 0 | for (DepGraphIndex i = 0; i < sim.size(); ++i) { |
418 | 0 | if ((mask >> i) & 1) { |
419 | 0 | set.Set(i); |
420 | 0 | } |
421 | 0 | } |
422 | 0 | return set; |
423 | 0 | }; |
424 | | |
425 | | /** Propagate ancestor information in sim. */ |
426 | 0 | auto anc_update_fn = [&]() { |
427 | 0 | while (true) { |
428 | 0 | bool updates{false}; |
429 | 0 | for (DepGraphIndex chl = 0; chl < sim.size(); ++chl) { |
430 | 0 | if (!sim[chl].has_value()) continue; |
431 | 0 | for (auto par : sim[chl]->second) { |
432 | 0 | if (!sim[chl]->second.IsSupersetOf(sim[par]->second)) { |
433 | 0 | sim[chl]->second |= sim[par]->second; |
434 | 0 | updates = true; |
435 | 0 | } |
436 | 0 | } |
437 | 0 | } |
438 | 0 | if (!updates) break; |
439 | 0 | } |
440 | 0 | }; |
441 | | |
442 | | /** Compare the state of transaction i in the simulation with the real one. */ |
443 | 0 | auto check_fn = [&](DepGraphIndex i) { |
444 | | // Compare used positions. |
445 | 0 | assert(real.Positions()[i] == sim[i].has_value()); |
446 | 0 | if (sim[i].has_value()) { |
447 | | // Compare feerate. |
448 | 0 | assert(real.FeeRate(i) == sim[i]->first); |
449 | | // Compare ancestors (note that SanityCheck verifies correspondence between ancestors |
450 | | // and descendants, so we can restrict ourselves to ancestors here). |
451 | 0 | assert(real.Ancestors(i) == sim[i]->second); |
452 | 0 | } |
453 | 0 | }; |
454 | |
|
455 | 0 | auto last_compaction_pos{real.PositionRange()}; |
456 | |
|
457 | 0 | LIMITED_WHILE(provider.remaining_bytes() > 0, 1000) { |
458 | 0 | int command = provider.ConsumeIntegral<uint8_t>() % 4; |
459 | 0 | while (true) { |
460 | | // Iterate decreasing command until an applicable branch is found. |
461 | 0 | if (num_tx_sim < TestBitSet::Size() && command-- == 0) { |
462 | | // AddTransaction. |
463 | 0 | auto fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff); |
464 | 0 | auto size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff); |
465 | 0 | FeeFrac feerate{fee, size}; |
466 | | // Apply to DepGraph. |
467 | 0 | auto idx = real.AddTransaction(feerate); |
468 | | // Verify that the returned index is correct. |
469 | 0 | assert(!sim[idx].has_value()); |
470 | 0 | for (DepGraphIndex i = 0; i < TestBitSet::Size(); ++i) { |
471 | 0 | if (!sim[i].has_value()) { |
472 | 0 | assert(idx == i); |
473 | 0 | break; |
474 | 0 | } |
475 | 0 | } |
476 | | // Update sim. |
477 | 0 | sim[idx] = {feerate, TestBitSet::Singleton(idx)}; |
478 | 0 | ++num_tx_sim; |
479 | 0 | break; |
480 | 0 | } else if (num_tx_sim > 0 && command-- == 0) { |
481 | | // AddDependencies. |
482 | 0 | DepGraphIndex child = idx_fn(); |
483 | 0 | auto parents = subset_fn(); |
484 | | // Apply to DepGraph. |
485 | 0 | real.AddDependencies(parents, child); |
486 | | // Apply to sim. |
487 | 0 | sim[child]->second |= parents; |
488 | 0 | break; |
489 | 0 | } else if (num_tx_sim > 0 && command-- == 0) { |
490 | | // Remove transactions. |
491 | 0 | auto del = set_fn(); |
492 | | // Propagate all ancestry information before deleting anything in the simulation (as |
493 | | // intermediary transactions may be deleted which impact connectivity). |
494 | 0 | anc_update_fn(); |
495 | | // Compare the state of the transactions being deleted. |
496 | 0 | for (auto i : del) check_fn(i); |
497 | | // Apply to DepGraph. |
498 | 0 | real.RemoveTransactions(del); |
499 | | // Apply to sim. |
500 | 0 | for (DepGraphIndex i = 0; i < sim.size(); ++i) { |
501 | 0 | if (sim[i].has_value()) { |
502 | 0 | if (del[i]) { |
503 | 0 | --num_tx_sim; |
504 | 0 | sim[i] = std::nullopt; |
505 | 0 | } else { |
506 | 0 | sim[i]->second -= del; |
507 | 0 | } |
508 | 0 | } |
509 | 0 | } |
510 | 0 | break; |
511 | 0 | } else if (command-- == 0) { |
512 | | // Compact. |
513 | 0 | const size_t mem_before{real.DynamicMemoryUsage()}; |
514 | 0 | real.Compact(); |
515 | 0 | const size_t mem_after{real.DynamicMemoryUsage()}; |
516 | 0 | assert(real.PositionRange() < last_compaction_pos ? mem_after < mem_before : mem_after <= mem_before); |
517 | 0 | last_compaction_pos = real.PositionRange(); |
518 | 0 | break; |
519 | 0 | } |
520 | 0 | } |
521 | 0 | } |
522 | | |
523 | | // Compare the real obtained depgraph against the simulation. |
524 | 0 | anc_update_fn(); |
525 | 0 | for (DepGraphIndex i = 0; i < sim.size(); ++i) check_fn(i); |
526 | 0 | assert(real.TxCount() == num_tx_sim); |
527 | | // Sanity check the result (which includes round-tripping serialization, if applicable). |
528 | 0 | SanityCheck(real); |
529 | 0 | } |
530 | | |
531 | | FUZZ_TARGET(clusterlin_depgraph_serialization) |
532 | 0 | { |
533 | | // Verify that any deserialized depgraph is acyclic and roundtrips to an identical depgraph. |
534 | | |
535 | | // Construct a graph by deserializing. |
536 | 0 | SpanReader reader(buffer); |
537 | 0 | DepGraph<TestBitSet> depgraph; |
538 | 0 | DepGraphIndex par_code{0}, chl_code{0}; |
539 | 0 | try { |
540 | 0 | reader >> Using<DepGraphFormatter>(depgraph) >> VARINT(par_code) >> VARINT(chl_code); |
541 | 0 | } catch (const std::ios_base::failure&) {} |
542 | 0 | SanityCheck(depgraph); |
543 | | |
544 | | // Verify the graph is a DAG. |
545 | 0 | assert(depgraph.IsAcyclic()); |
546 | | |
547 | | // Introduce a cycle, and then test that IsAcyclic returns false. |
548 | 0 | if (depgraph.TxCount() < 2) return; |
549 | 0 | DepGraphIndex par(0), chl(0); |
550 | | // Pick any transaction of depgraph as parent. |
551 | 0 | par_code %= depgraph.TxCount(); |
552 | 0 | for (auto i : depgraph.Positions()) { |
553 | 0 | if (par_code == 0) { |
554 | 0 | par = i; |
555 | 0 | break; |
556 | 0 | } |
557 | 0 | --par_code; |
558 | 0 | } |
559 | | // Pick any ancestor of par (excluding itself) as child, if any. |
560 | 0 | auto ancestors = depgraph.Ancestors(par) - TestBitSet::Singleton(par); |
561 | 0 | if (ancestors.None()) return; |
562 | 0 | chl_code %= ancestors.Count(); |
563 | 0 | for (auto i : ancestors) { |
564 | 0 | if (chl_code == 0) { |
565 | 0 | chl = i; |
566 | 0 | break; |
567 | 0 | } |
568 | 0 | --chl_code; |
569 | 0 | } |
570 | | // Add the cycle-introducing dependency. |
571 | 0 | depgraph.AddDependencies(TestBitSet::Singleton(par), chl); |
572 | | // Check that we now detect a cycle. |
573 | 0 | assert(!depgraph.IsAcyclic()); |
574 | 0 | } |
575 | | |
576 | | FUZZ_TARGET(clusterlin_components) |
577 | 0 | { |
578 | | // Verify the behavior of DepGraphs's FindConnectedComponent and IsConnected functions. |
579 | | |
580 | | // Construct a depgraph. |
581 | 0 | SpanReader reader(buffer); |
582 | 0 | DepGraph<TestBitSet> depgraph; |
583 | 0 | std::vector<DepGraphIndex> linearization; |
584 | 0 | try { |
585 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
586 | 0 | } catch (const std::ios_base::failure&) {} |
587 | |
|
588 | 0 | TestBitSet todo = depgraph.Positions(); |
589 | 0 | while (todo.Any()) { |
590 | | // Pick a transaction in todo, or nothing. |
591 | 0 | std::optional<DepGraphIndex> picked; |
592 | 0 | { |
593 | 0 | uint64_t picked_num{0}; |
594 | 0 | try { |
595 | 0 | reader >> VARINT(picked_num); |
596 | 0 | } catch (const std::ios_base::failure&) {} |
597 | 0 | if (picked_num < todo.Size() && todo[picked_num]) { |
598 | 0 | picked = picked_num; |
599 | 0 | } |
600 | 0 | } |
601 | | |
602 | | // Find a connected component inside todo, including picked if any. |
603 | 0 | auto component = picked ? depgraph.GetConnectedComponent(todo, *picked) |
604 | 0 | : depgraph.FindConnectedComponent(todo); |
605 | | |
606 | | // The component must be a subset of todo and non-empty. |
607 | 0 | assert(component.IsSubsetOf(todo)); |
608 | 0 | assert(component.Any()); |
609 | | |
610 | | // If picked was provided, the component must include it. |
611 | 0 | if (picked) assert(component[*picked]); |
612 | | |
613 | | // If todo is the entire graph, and the entire graph is connected, then the component must |
614 | | // be the entire graph. |
615 | 0 | if (todo == depgraph.Positions()) { |
616 | 0 | assert((component == todo) == depgraph.IsConnected()); |
617 | 0 | } |
618 | | |
619 | | // If subset is connected, then component must match subset. |
620 | 0 | assert((component == todo) == depgraph.IsConnected(todo)); |
621 | | |
622 | | // The component cannot have any ancestors or descendants outside of component but in todo. |
623 | 0 | for (auto i : component) { |
624 | 0 | assert((depgraph.Ancestors(i) & todo).IsSubsetOf(component)); |
625 | 0 | assert((depgraph.Descendants(i) & todo).IsSubsetOf(component)); |
626 | 0 | } |
627 | | |
628 | | // Starting from any component element, we must be able to reach every element. |
629 | 0 | for (auto i : component) { |
630 | | // Start with just i as reachable. |
631 | 0 | TestBitSet reachable = TestBitSet::Singleton(i); |
632 | | // Add in-todo descendants and ancestors to reachable until it does not change anymore. |
633 | 0 | while (true) { |
634 | 0 | TestBitSet new_reachable = reachable; |
635 | 0 | for (auto j : new_reachable) { |
636 | 0 | new_reachable |= depgraph.Ancestors(j) & todo; |
637 | 0 | new_reachable |= depgraph.Descendants(j) & todo; |
638 | 0 | } |
639 | 0 | if (new_reachable == reachable) break; |
640 | 0 | reachable = new_reachable; |
641 | 0 | } |
642 | | // Verify that the result is the entire component. |
643 | 0 | assert(component == reachable); |
644 | 0 | } |
645 | | |
646 | | // Construct an arbitrary subset of todo. |
647 | 0 | uint64_t subset_bits{0}; |
648 | 0 | try { |
649 | 0 | reader >> VARINT(subset_bits); |
650 | 0 | } catch (const std::ios_base::failure&) {} |
651 | 0 | TestBitSet subset; |
652 | 0 | for (DepGraphIndex i : depgraph.Positions()) { |
653 | 0 | if (todo[i]) { |
654 | 0 | if (subset_bits & 1) subset.Set(i); |
655 | 0 | subset_bits >>= 1; |
656 | 0 | } |
657 | 0 | } |
658 | | // Which must be non-empty. |
659 | 0 | if (subset.None()) subset = TestBitSet::Singleton(todo.First()); |
660 | | // Remove it from todo. |
661 | 0 | todo -= subset; |
662 | 0 | } |
663 | | |
664 | | // No components can be found in an empty subset. |
665 | 0 | assert(depgraph.FindConnectedComponent(todo).None()); |
666 | 0 | } |
667 | | |
668 | | FUZZ_TARGET(clusterlin_make_connected) |
669 | 0 | { |
670 | | // Verify that MakeConnected makes graphs connected. |
671 | |
|
672 | 0 | SpanReader reader(buffer); |
673 | 0 | DepGraph<TestBitSet> depgraph; |
674 | 0 | try { |
675 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
676 | 0 | } catch (const std::ios_base::failure&) {} |
677 | 0 | MakeConnected(depgraph); |
678 | 0 | SanityCheck(depgraph); |
679 | 0 | assert(depgraph.IsConnected()); |
680 | 0 | } |
681 | | |
682 | | FUZZ_TARGET(clusterlin_chunking) |
683 | 0 | { |
684 | | // Verify the correctness of the ChunkLinearization function. |
685 | | |
686 | | // Construct a graph by deserializing. |
687 | 0 | SpanReader reader(buffer); |
688 | 0 | DepGraph<TestBitSet> depgraph; |
689 | 0 | try { |
690 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
691 | 0 | } catch (const std::ios_base::failure&) {} |
692 | | |
693 | | // Read a valid linearization for depgraph. |
694 | 0 | auto linearization = ReadLinearization(depgraph, reader); |
695 | | |
696 | | // Invoke the chunking function. |
697 | 0 | auto chunking = ChunkLinearization(depgraph, linearization); |
698 | | |
699 | | // Verify that chunk feerates are monotonically non-increasing. |
700 | 0 | for (size_t i = 1; i < chunking.size(); ++i) { |
701 | 0 | assert(!(chunking[i] >> chunking[i - 1])); |
702 | 0 | } |
703 | | |
704 | | // Naively recompute the chunks (each is the highest-feerate prefix of what remains). |
705 | 0 | auto todo = depgraph.Positions(); |
706 | 0 | for (const auto& chunk_feerate : chunking) { |
707 | 0 | assert(todo.Any()); |
708 | 0 | SetInfo<TestBitSet> accumulator, best; |
709 | 0 | for (DepGraphIndex idx : linearization) { |
710 | 0 | if (todo[idx]) { |
711 | 0 | accumulator.Set(depgraph, idx); |
712 | 0 | if (best.feerate.IsEmpty() || accumulator.feerate >> best.feerate) { |
713 | 0 | best = accumulator; |
714 | 0 | } |
715 | 0 | } |
716 | 0 | } |
717 | 0 | assert(chunk_feerate == best.feerate); |
718 | 0 | assert(best.transactions.IsSubsetOf(todo)); |
719 | 0 | todo -= best.transactions; |
720 | 0 | } |
721 | 0 | assert(todo.None()); |
722 | 0 | } |
723 | | |
724 | | FUZZ_TARGET(clusterlin_ancestor_finder) |
725 | 0 | { |
726 | | // Verify that AncestorCandidateFinder works as expected. |
727 | | |
728 | | // Retrieve a depgraph from the fuzz input. |
729 | 0 | SpanReader reader(buffer); |
730 | 0 | DepGraph<TestBitSet> depgraph; |
731 | 0 | try { |
732 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
733 | 0 | } catch (const std::ios_base::failure&) {} |
734 | |
|
735 | 0 | AncestorCandidateFinder anc_finder(depgraph); |
736 | 0 | auto todo = depgraph.Positions(); |
737 | 0 | while (todo.Any()) { |
738 | | // Call the ancestor finder's FindCandidateSet for what remains of the graph. |
739 | 0 | assert(!anc_finder.AllDone()); |
740 | 0 | assert(todo.Count() == anc_finder.NumRemaining()); |
741 | 0 | auto best_anc = anc_finder.FindCandidateSet(); |
742 | | // Sanity check the result. |
743 | 0 | assert(best_anc.transactions.Any()); |
744 | 0 | assert(best_anc.transactions.IsSubsetOf(todo)); |
745 | 0 | assert(depgraph.FeeRate(best_anc.transactions) == best_anc.feerate); |
746 | 0 | assert(depgraph.IsConnected(best_anc.transactions)); |
747 | | // Check that it is topologically valid. |
748 | 0 | for (auto i : best_anc.transactions) { |
749 | 0 | assert((depgraph.Ancestors(i) & todo).IsSubsetOf(best_anc.transactions)); |
750 | 0 | } |
751 | | |
752 | | // Compute all remaining ancestor sets. |
753 | 0 | std::optional<SetInfo<TestBitSet>> real_best_anc; |
754 | 0 | for (auto i : todo) { |
755 | 0 | SetInfo info(depgraph, todo & depgraph.Ancestors(i)); |
756 | 0 | if (!real_best_anc.has_value() || info.feerate > real_best_anc->feerate) { |
757 | 0 | real_best_anc = info; |
758 | 0 | } |
759 | 0 | } |
760 | | // The set returned by anc_finder must equal the real best ancestor sets. |
761 | 0 | assert(real_best_anc.has_value()); |
762 | 0 | assert(*real_best_anc == best_anc); |
763 | | |
764 | | // Find a non-empty topologically valid subset of transactions to remove from the graph. |
765 | | // Using an empty set would mean the next iteration is identical to the current one, and |
766 | | // could cause an infinite loop. |
767 | 0 | auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
768 | 0 | todo -= del_set; |
769 | 0 | anc_finder.MarkDone(del_set); |
770 | 0 | } |
771 | 0 | assert(anc_finder.AllDone()); |
772 | 0 | assert(anc_finder.NumRemaining() == 0); |
773 | 0 | } |
774 | | |
775 | | static constexpr auto MAX_SIMPLE_ITERATIONS = 300000; |
776 | | |
777 | | FUZZ_TARGET(clusterlin_simple_finder) |
778 | 0 | { |
779 | | // Verify that SimpleCandidateFinder works as expected by sanity checking the results |
780 | | // and comparing them (if claimed to be optimal) against the sets found by |
781 | | // ExhaustiveCandidateFinder and AncestorCandidateFinder. |
782 | | // |
783 | | // Note that SimpleCandidateFinder is only used in tests; the purpose of this fuzz test is to |
784 | | // establish confidence in SimpleCandidateFinder, so that it can be used to test |
785 | | // SearchCandidateFinder below. |
786 | | |
787 | | // Retrieve a depgraph from the fuzz input. |
788 | 0 | SpanReader reader(buffer); |
789 | 0 | DepGraph<TestBitSet> depgraph; |
790 | 0 | try { |
791 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
792 | 0 | } catch (const std::ios_base::failure&) {} |
793 | | |
794 | | // Instantiate the SimpleCandidateFinder to be tested, and the ExhaustiveCandidateFinder and |
795 | | // AncestorCandidateFinder it is being tested against. |
796 | 0 | SimpleCandidateFinder smp_finder(depgraph); |
797 | 0 | ExhaustiveCandidateFinder exh_finder(depgraph); |
798 | 0 | AncestorCandidateFinder anc_finder(depgraph); |
799 | |
|
800 | 0 | auto todo = depgraph.Positions(); |
801 | 0 | while (todo.Any()) { |
802 | 0 | assert(!smp_finder.AllDone()); |
803 | 0 | assert(!exh_finder.AllDone()); |
804 | 0 | assert(!anc_finder.AllDone()); |
805 | 0 | assert(anc_finder.NumRemaining() == todo.Count()); |
806 | | |
807 | | // Call SimpleCandidateFinder. |
808 | 0 | auto [found, iterations_done] = smp_finder.FindCandidateSet(MAX_SIMPLE_ITERATIONS); |
809 | 0 | bool optimal = (iterations_done != MAX_SIMPLE_ITERATIONS); |
810 | | |
811 | | // Sanity check the result. |
812 | 0 | assert(iterations_done <= MAX_SIMPLE_ITERATIONS); |
813 | 0 | assert(found.transactions.Any()); |
814 | 0 | assert(found.transactions.IsSubsetOf(todo)); |
815 | 0 | assert(depgraph.FeeRate(found.transactions) == found.feerate); |
816 | | // Check that it is topologically valid. |
817 | 0 | for (auto i : found.transactions) { |
818 | 0 | assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo)); |
819 | 0 | } |
820 | | |
821 | | // At most 2^(N-1) iterations can be required: the number of non-empty connected subsets a |
822 | | // graph with N transactions can have. If MAX_SIMPLE_ITERATIONS exceeds this number, the |
823 | | // result is necessarily optimal. |
824 | 0 | assert(iterations_done <= (uint64_t{1} << (todo.Count() - 1))); |
825 | 0 | if (MAX_SIMPLE_ITERATIONS > (uint64_t{1} << (todo.Count() - 1))) assert(optimal); |
826 | | |
827 | | // SimpleCandidateFinder only finds connected sets. |
828 | 0 | assert(depgraph.IsConnected(found.transactions)); |
829 | | |
830 | | // Perform further quality checks only if SimpleCandidateFinder claims an optimal result. |
831 | 0 | if (optimal) { |
832 | | // Compare with AncestorCandidateFinder. |
833 | 0 | auto anc = anc_finder.FindCandidateSet(); |
834 | 0 | assert(anc.feerate <= found.feerate); |
835 | | |
836 | 0 | if (todo.Count() <= 12) { |
837 | | // Compare with ExhaustiveCandidateFinder. This quickly gets computationally |
838 | | // expensive for large clusters (O(2^n)), so only do it for sufficiently small ones. |
839 | 0 | auto exhaustive = exh_finder.FindCandidateSet(); |
840 | 0 | assert(exhaustive.feerate == found.feerate); |
841 | 0 | } |
842 | | |
843 | | // Compare with a non-empty topological set read from the fuzz input (comparing with an |
844 | | // empty set is not interesting). |
845 | 0 | auto read_topo = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
846 | 0 | assert(found.feerate >= depgraph.FeeRate(read_topo)); |
847 | 0 | } |
848 | | |
849 | | // Find a non-empty topologically valid subset of transactions to remove from the graph. |
850 | | // Using an empty set would mean the next iteration is identical to the current one, and |
851 | | // could cause an infinite loop. |
852 | 0 | auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
853 | 0 | todo -= del_set; |
854 | 0 | smp_finder.MarkDone(del_set); |
855 | 0 | exh_finder.MarkDone(del_set); |
856 | 0 | anc_finder.MarkDone(del_set); |
857 | 0 | } |
858 | | |
859 | 0 | assert(smp_finder.AllDone()); |
860 | 0 | assert(exh_finder.AllDone()); |
861 | 0 | assert(anc_finder.AllDone()); |
862 | 0 | assert(anc_finder.NumRemaining() == 0); |
863 | 0 | } |
864 | | |
865 | | FUZZ_TARGET(clusterlin_search_finder) |
866 | 0 | { |
867 | | // Verify that SearchCandidateFinder works as expected by sanity checking the results |
868 | | // and comparing with the results from SimpleCandidateFinder and AncestorCandidateFinder, |
869 | | // if the result is claimed to be optimal. |
870 | | |
871 | | // Retrieve an RNG seed, a depgraph, and whether to make it connected, from the fuzz input. |
872 | 0 | SpanReader reader(buffer); |
873 | 0 | DepGraph<TestBitSet> depgraph; |
874 | 0 | uint64_t rng_seed{0}; |
875 | 0 | uint8_t make_connected{1}; |
876 | 0 | try { |
877 | 0 | reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected; |
878 | 0 | } catch (const std::ios_base::failure&) {} |
879 | | // The most complicated graphs are connected ones (other ones just split up). Optionally force |
880 | | // the graph to be connected. |
881 | 0 | if (make_connected) MakeConnected(depgraph); |
882 | | |
883 | | // Instantiate the candidate finders. |
884 | 0 | SearchCandidateFinder src_finder(depgraph, rng_seed); |
885 | 0 | SimpleCandidateFinder smp_finder(depgraph); |
886 | 0 | AncestorCandidateFinder anc_finder(depgraph); |
887 | |
|
888 | 0 | auto todo = depgraph.Positions(); |
889 | 0 | while (todo.Any()) { |
890 | 0 | assert(!src_finder.AllDone()); |
891 | 0 | assert(!smp_finder.AllDone()); |
892 | 0 | assert(!anc_finder.AllDone()); |
893 | 0 | assert(anc_finder.NumRemaining() == todo.Count()); |
894 | | |
895 | | // For each iteration, read an iteration count limit from the fuzz input. |
896 | 0 | uint64_t max_iterations = 1; |
897 | 0 | try { |
898 | 0 | reader >> VARINT(max_iterations); |
899 | 0 | } catch (const std::ios_base::failure&) {} |
900 | 0 | max_iterations &= 0xfffff; |
901 | | |
902 | | // Read an initial subset from the fuzz input (allowed to be empty). |
903 | 0 | auto init_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/false); |
904 | 0 | SetInfo init_best(depgraph, init_set); |
905 | | |
906 | | // Call the search finder's FindCandidateSet for what remains of the graph. |
907 | 0 | auto [found, iterations_done] = src_finder.FindCandidateSet(max_iterations, init_best); |
908 | 0 | bool optimal = iterations_done < max_iterations; |
909 | | |
910 | | // Sanity check the result. |
911 | 0 | assert(iterations_done <= max_iterations); |
912 | 0 | assert(found.transactions.Any()); |
913 | 0 | assert(found.transactions.IsSubsetOf(todo)); |
914 | 0 | assert(depgraph.FeeRate(found.transactions) == found.feerate); |
915 | 0 | if (!init_best.feerate.IsEmpty()) assert(found.feerate >= init_best.feerate); |
916 | | // Check that it is topologically valid. |
917 | 0 | for (auto i : found.transactions) { |
918 | 0 | assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo)); |
919 | 0 | } |
920 | | |
921 | | // At most 2^(N-1) iterations can be required: the maximum number of non-empty topological |
922 | | // subsets a (connected) cluster with N transactions can have. Even when the cluster is no |
923 | | // longer connected after removing certain transactions, this holds, because the connected |
924 | | // components are searched separately. |
925 | 0 | assert(iterations_done <= (uint64_t{1} << (todo.Count() - 1))); |
926 | | // Additionally, test that no more than sqrt(2^N)+1 iterations are required. This is just |
927 | | // an empirical bound that seems to hold, without proof. Still, add a test for it so we |
928 | | // can learn about counterexamples if they exist. |
929 | 0 | if (iterations_done >= 1 && todo.Count() <= 63) { |
930 | 0 | Assume((iterations_done - 1) * (iterations_done - 1) <= uint64_t{1} << todo.Count()); |
931 | 0 | } |
932 | | |
933 | | // Perform quality checks only if SearchCandidateFinder claims an optimal result. |
934 | 0 | if (optimal) { |
935 | | // Optimal sets are always connected. |
936 | 0 | assert(depgraph.IsConnected(found.transactions)); |
937 | | |
938 | | // Compare with SimpleCandidateFinder. |
939 | 0 | auto [simple, simple_iters] = smp_finder.FindCandidateSet(MAX_SIMPLE_ITERATIONS); |
940 | 0 | assert(found.feerate >= simple.feerate); |
941 | 0 | if (simple_iters < MAX_SIMPLE_ITERATIONS) { |
942 | 0 | assert(found.feerate == simple.feerate); |
943 | 0 | } |
944 | | |
945 | | // Compare with AncestorCandidateFinder; |
946 | 0 | auto anc = anc_finder.FindCandidateSet(); |
947 | 0 | assert(found.feerate >= anc.feerate); |
948 | | |
949 | | // Compare with a non-empty topological set read from the fuzz input (comparing with an |
950 | | // empty set is not interesting). |
951 | 0 | auto read_topo = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
952 | 0 | assert(found.feerate >= depgraph.FeeRate(read_topo)); |
953 | 0 | } |
954 | | |
955 | | // Find a non-empty topologically valid subset of transactions to remove from the graph. |
956 | | // Using an empty set would mean the next iteration is identical to the current one, and |
957 | | // could cause an infinite loop. |
958 | 0 | auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
959 | 0 | todo -= del_set; |
960 | 0 | src_finder.MarkDone(del_set); |
961 | 0 | smp_finder.MarkDone(del_set); |
962 | 0 | anc_finder.MarkDone(del_set); |
963 | 0 | } |
964 | | |
965 | 0 | assert(src_finder.AllDone()); |
966 | 0 | assert(smp_finder.AllDone()); |
967 | 0 | assert(anc_finder.AllDone()); |
968 | 0 | assert(anc_finder.NumRemaining() == 0); |
969 | 0 | } |
970 | | |
971 | | FUZZ_TARGET(clusterlin_linearization_chunking) |
972 | 0 | { |
973 | | // Verify the behavior of LinearizationChunking. |
974 | | |
975 | | // Retrieve a depgraph from the fuzz input. |
976 | 0 | SpanReader reader(buffer); |
977 | 0 | DepGraph<TestBitSet> depgraph; |
978 | 0 | try { |
979 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
980 | 0 | } catch (const std::ios_base::failure&) {} |
981 | | |
982 | | // Retrieve a topologically-valid subset of depgraph (allowed to be empty, because the argument |
983 | | // to LinearizationChunking::Intersect is allowed to be empty). |
984 | 0 | auto todo = depgraph.Positions(); |
985 | 0 | auto subset = SetInfo(depgraph, ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/false)); |
986 | | |
987 | | // Retrieve a valid linearization for depgraph. |
988 | 0 | auto linearization = ReadLinearization(depgraph, reader); |
989 | | |
990 | | // Construct a LinearizationChunking object, initially for the whole linearization. |
991 | 0 | LinearizationChunking chunking(depgraph, linearization); |
992 | | |
993 | | // Incrementally remove transactions from the chunking object, and check various properties at |
994 | | // every step. |
995 | 0 | while (todo.Any()) { |
996 | 0 | assert(chunking.NumChunksLeft() > 0); |
997 | | |
998 | | // Construct linearization with just todo. |
999 | 0 | std::vector<DepGraphIndex> linearization_left; |
1000 | 0 | for (auto i : linearization) { |
1001 | 0 | if (todo[i]) linearization_left.push_back(i); |
1002 | 0 | } |
1003 | | |
1004 | | // Compute the chunking for linearization_left. |
1005 | 0 | auto chunking_left = ChunkLinearization(depgraph, linearization_left); |
1006 | | |
1007 | | // Verify that it matches the feerates of the chunks of chunking. |
1008 | 0 | assert(chunking.NumChunksLeft() == chunking_left.size()); |
1009 | 0 | for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) { |
1010 | 0 | assert(chunking.GetChunk(i).feerate == chunking_left[i]); |
1011 | 0 | } |
1012 | | |
1013 | | // Check consistency of chunking. |
1014 | 0 | TestBitSet combined; |
1015 | 0 | for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) { |
1016 | 0 | const auto& chunk_info = chunking.GetChunk(i); |
1017 | | // Chunks must be non-empty. |
1018 | 0 | assert(chunk_info.transactions.Any()); |
1019 | | // Chunk feerates must be monotonically non-increasing. |
1020 | 0 | if (i > 0) assert(!(chunk_info.feerate >> chunking.GetChunk(i - 1).feerate)); |
1021 | | // Chunks must be a subset of what is left of the linearization. |
1022 | 0 | assert(chunk_info.transactions.IsSubsetOf(todo)); |
1023 | | // Chunks' claimed feerates must match their transactions' aggregate feerate. |
1024 | 0 | assert(depgraph.FeeRate(chunk_info.transactions) == chunk_info.feerate); |
1025 | | // Chunks must be the highest-feerate remaining prefix. |
1026 | 0 | SetInfo<TestBitSet> accumulator, best; |
1027 | 0 | for (auto j : linearization) { |
1028 | 0 | if (todo[j] && !combined[j]) { |
1029 | 0 | accumulator.Set(depgraph, j); |
1030 | 0 | if (best.feerate.IsEmpty() || accumulator.feerate > best.feerate) { |
1031 | 0 | best = accumulator; |
1032 | 0 | } |
1033 | 0 | } |
1034 | 0 | } |
1035 | 0 | assert(best.transactions == chunk_info.transactions); |
1036 | 0 | assert(best.feerate == chunk_info.feerate); |
1037 | | // Chunks cannot overlap. |
1038 | 0 | assert(!chunk_info.transactions.Overlaps(combined)); |
1039 | 0 | combined |= chunk_info.transactions; |
1040 | | // Chunks must be topological. |
1041 | 0 | for (auto idx : chunk_info.transactions) { |
1042 | 0 | assert((depgraph.Ancestors(idx) & todo).IsSubsetOf(combined)); |
1043 | 0 | } |
1044 | 0 | } |
1045 | 0 | assert(combined == todo); |
1046 | | |
1047 | | // Verify the expected properties of LinearizationChunking::IntersectPrefixes: |
1048 | 0 | auto intersect = chunking.IntersectPrefixes(subset); |
1049 | | // - Intersecting again doesn't change the result. |
1050 | 0 | assert(chunking.IntersectPrefixes(intersect) == intersect); |
1051 | | // - The intersection is topological. |
1052 | 0 | TestBitSet intersect_anc; |
1053 | 0 | for (auto idx : intersect.transactions) { |
1054 | 0 | intersect_anc |= (depgraph.Ancestors(idx) & todo); |
1055 | 0 | } |
1056 | 0 | assert(intersect.transactions == intersect_anc); |
1057 | | // - The claimed intersection feerate matches its transactions. |
1058 | 0 | assert(intersect.feerate == depgraph.FeeRate(intersect.transactions)); |
1059 | | // - The intersection may only be empty if its input is empty. |
1060 | 0 | assert(intersect.transactions.Any() == subset.transactions.Any()); |
1061 | | // - The intersection feerate must be as high as the input. |
1062 | 0 | assert(intersect.feerate >= subset.feerate); |
1063 | | // - No non-empty intersection between the intersection and a prefix of the chunks of the |
1064 | | // remainder of the linearization may be better than the intersection. |
1065 | 0 | TestBitSet prefix; |
1066 | 0 | for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) { |
1067 | 0 | prefix |= chunking.GetChunk(i).transactions; |
1068 | 0 | auto reintersect = SetInfo(depgraph, prefix & intersect.transactions); |
1069 | 0 | if (!reintersect.feerate.IsEmpty()) { |
1070 | 0 | assert(reintersect.feerate <= intersect.feerate); |
1071 | 0 | } |
1072 | 0 | } |
1073 | | |
1074 | | // Find a non-empty topologically valid subset of transactions to remove from the graph. |
1075 | | // Using an empty set would mean the next iteration is identical to the current one, and |
1076 | | // could cause an infinite loop. |
1077 | 0 | auto done = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true); |
1078 | 0 | todo -= done; |
1079 | 0 | chunking.MarkDone(done); |
1080 | 0 | subset = SetInfo(depgraph, subset.transactions - done); |
1081 | 0 | } |
1082 | | |
1083 | 0 | assert(chunking.NumChunksLeft() == 0); |
1084 | 0 | } |
1085 | | |
1086 | | FUZZ_TARGET(clusterlin_simple_linearize) |
1087 | 0 | { |
1088 | | // Verify the behavior of SimpleLinearize(). Note that SimpleLinearize is only used in tests; |
1089 | | // the purpose of this fuzz test is to establish confidence in SimpleLinearize, so that it can |
1090 | | // be used to test the real Linearize function in the fuzz test below. |
1091 | | |
1092 | | // Retrieve an iteration count and a depgraph from the fuzz input. |
1093 | 0 | SpanReader reader(buffer); |
1094 | 0 | uint64_t iter_count{0}; |
1095 | 0 | DepGraph<TestBitSet> depgraph; |
1096 | 0 | try { |
1097 | 0 | reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph); |
1098 | 0 | } catch (const std::ios_base::failure&) {} |
1099 | 0 | iter_count %= MAX_SIMPLE_ITERATIONS; |
1100 | | |
1101 | | // Invoke SimpleLinearize(). |
1102 | 0 | auto [linearization, optimal] = SimpleLinearize(depgraph, iter_count); |
1103 | 0 | SanityCheck(depgraph, linearization); |
1104 | 0 | auto simple_chunking = ChunkLinearization(depgraph, linearization); |
1105 | | |
1106 | | // If the iteration count is sufficiently high, an optimal linearization must be found. |
1107 | | // SimpleLinearize on k transactions can take up to 2^(k-1) iterations (one per non-empty |
1108 | | // connected topologically valid subset), which sums over k=1..n to (2^n)-1. |
1109 | 0 | const uint64_t n = depgraph.TxCount(); |
1110 | 0 | if (n <= 63 && (iter_count >> n)) { |
1111 | 0 | assert(optimal); |
1112 | 0 | } |
1113 | | |
1114 | | // If SimpleLinearize claims optimal result, and the cluster is sufficiently small (there are |
1115 | | // n! linearizations), test that the result is as good as every valid linearization. |
1116 | 0 | if (optimal && depgraph.TxCount() <= 8) { |
1117 | 0 | auto exh_linearization = ExhaustiveLinearize(depgraph); |
1118 | 0 | auto exh_chunking = ChunkLinearization(depgraph, exh_linearization); |
1119 | 0 | auto cmp = CompareChunks(simple_chunking, exh_chunking); |
1120 | 0 | assert(cmp == 0); |
1121 | 0 | assert(simple_chunking.size() == exh_chunking.size()); |
1122 | 0 | } |
1123 | | |
1124 | 0 | if (optimal) { |
1125 | | // Compare with a linearization read from the fuzz input. |
1126 | 0 | auto read = ReadLinearization(depgraph, reader); |
1127 | 0 | auto read_chunking = ChunkLinearization(depgraph, read); |
1128 | 0 | auto cmp = CompareChunks(simple_chunking, read_chunking); |
1129 | 0 | assert(cmp >= 0); |
1130 | 0 | } |
1131 | 0 | } |
1132 | | |
1133 | | FUZZ_TARGET(clusterlin_linearize) |
1134 | 0 | { |
1135 | | // Verify the behavior of Linearize(). |
1136 | | |
1137 | | // Retrieve an RNG seed, an iteration count, a depgraph, and whether to make it connected from |
1138 | | // the fuzz input. |
1139 | 0 | SpanReader reader(buffer); |
1140 | 0 | DepGraph<TestBitSet> depgraph; |
1141 | 0 | uint64_t rng_seed{0}; |
1142 | 0 | uint64_t iter_count{0}; |
1143 | 0 | uint8_t make_connected{1}; |
1144 | 0 | try { |
1145 | 0 | reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected; |
1146 | 0 | } catch (const std::ios_base::failure&) {} |
1147 | | // The most complicated graphs are connected ones (other ones just split up). Optionally force |
1148 | | // the graph to be connected. |
1149 | 0 | if (make_connected) MakeConnected(depgraph); |
1150 | | |
1151 | | // Optionally construct an old linearization for it. |
1152 | 0 | std::vector<DepGraphIndex> old_linearization; |
1153 | 0 | { |
1154 | 0 | uint8_t have_old_linearization{0}; |
1155 | 0 | try { |
1156 | 0 | reader >> have_old_linearization; |
1157 | 0 | } catch(const std::ios_base::failure&) {} |
1158 | 0 | if (have_old_linearization & 1) { |
1159 | 0 | old_linearization = ReadLinearization(depgraph, reader); |
1160 | 0 | SanityCheck(depgraph, old_linearization); |
1161 | 0 | } |
1162 | 0 | } |
1163 | | |
1164 | | // Invoke Linearize(). |
1165 | 0 | iter_count &= 0x7ffff; |
1166 | 0 | auto [linearization, optimal, cost] = Linearize(depgraph, iter_count, rng_seed, old_linearization); |
1167 | 0 | assert(cost <= iter_count); |
1168 | 0 | SanityCheck(depgraph, linearization); |
1169 | 0 | auto chunking = ChunkLinearization(depgraph, linearization); |
1170 | | |
1171 | | // Linearization must always be as good as the old one, if provided. |
1172 | 0 | if (!old_linearization.empty()) { |
1173 | 0 | auto old_chunking = ChunkLinearization(depgraph, old_linearization); |
1174 | 0 | auto cmp = CompareChunks(chunking, old_chunking); |
1175 | 0 | assert(cmp >= 0); |
1176 | 0 | } |
1177 | | |
1178 | | // If the iteration count is sufficiently high, an optimal linearization must be found. |
1179 | 0 | if (iter_count >= MaxOptimalLinearizationIters(depgraph.TxCount())) { |
1180 | 0 | assert(optimal); |
1181 | 0 | } |
1182 | | |
1183 | | // If Linearize claims optimal result, run quality tests. |
1184 | 0 | if (optimal) { |
1185 | | // It must be as good as SimpleLinearize. |
1186 | 0 | auto [simple_linearization, simple_optimal] = SimpleLinearize(depgraph, MAX_SIMPLE_ITERATIONS); |
1187 | 0 | SanityCheck(depgraph, simple_linearization); |
1188 | 0 | auto simple_chunking = ChunkLinearization(depgraph, simple_linearization); |
1189 | 0 | auto cmp = CompareChunks(chunking, simple_chunking); |
1190 | 0 | assert(cmp >= 0); |
1191 | | // If SimpleLinearize finds the optimal result too, they must be equal (if not, |
1192 | | // SimpleLinearize is broken). |
1193 | 0 | if (simple_optimal) assert(cmp == 0); |
1194 | | // If simple_chunking is diagram-optimal, it cannot have more chunks than chunking (as |
1195 | | // chunking is claimed to be optimal, which implies minimal chunks). |
1196 | 0 | if (cmp == 0) assert(chunking.size() >= simple_chunking.size()); |
1197 | | |
1198 | | // Compare with a linearization read from the fuzz input. |
1199 | 0 | auto read = ReadLinearization(depgraph, reader); |
1200 | 0 | auto read_chunking = ChunkLinearization(depgraph, read); |
1201 | 0 | auto cmp_read = CompareChunks(chunking, read_chunking); |
1202 | 0 | assert(cmp_read >= 0); |
1203 | 0 | } |
1204 | 0 | } |
1205 | | |
1206 | | FUZZ_TARGET(clusterlin_postlinearize) |
1207 | 0 | { |
1208 | | // Verify expected properties of PostLinearize() on arbitrary linearizations. |
1209 | | |
1210 | | // Retrieve a depgraph from the fuzz input. |
1211 | 0 | SpanReader reader(buffer); |
1212 | 0 | DepGraph<TestBitSet> depgraph; |
1213 | 0 | try { |
1214 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
1215 | 0 | } catch (const std::ios_base::failure&) {} |
1216 | | |
1217 | | // Retrieve a linearization from the fuzz input. |
1218 | 0 | std::vector<DepGraphIndex> linearization; |
1219 | 0 | linearization = ReadLinearization(depgraph, reader); |
1220 | 0 | SanityCheck(depgraph, linearization); |
1221 | | |
1222 | | // Produce a post-processed version. |
1223 | 0 | auto post_linearization = linearization; |
1224 | 0 | PostLinearize(depgraph, post_linearization); |
1225 | 0 | SanityCheck(depgraph, post_linearization); |
1226 | | |
1227 | | // Compare diagrams: post-linearization cannot worsen anywhere. |
1228 | 0 | auto chunking = ChunkLinearization(depgraph, linearization); |
1229 | 0 | auto post_chunking = ChunkLinearization(depgraph, post_linearization); |
1230 | 0 | auto cmp = CompareChunks(post_chunking, chunking); |
1231 | 0 | assert(cmp >= 0); |
1232 | | |
1233 | | // Run again, things can keep improving (and never get worse) |
1234 | 0 | auto post_post_linearization = post_linearization; |
1235 | 0 | PostLinearize(depgraph, post_post_linearization); |
1236 | 0 | SanityCheck(depgraph, post_post_linearization); |
1237 | 0 | auto post_post_chunking = ChunkLinearization(depgraph, post_post_linearization); |
1238 | 0 | cmp = CompareChunks(post_post_chunking, post_chunking); |
1239 | 0 | assert(cmp >= 0); |
1240 | | |
1241 | | // The chunks that come out of postlinearizing are always connected. |
1242 | 0 | LinearizationChunking linchunking(depgraph, post_linearization); |
1243 | 0 | while (linchunking.NumChunksLeft()) { |
1244 | 0 | assert(depgraph.IsConnected(linchunking.GetChunk(0).transactions)); |
1245 | 0 | linchunking.MarkDone(linchunking.GetChunk(0).transactions); |
1246 | 0 | } |
1247 | 0 | } |
1248 | | |
1249 | | FUZZ_TARGET(clusterlin_postlinearize_tree) |
1250 | 0 | { |
1251 | | // Verify expected properties of PostLinearize() on linearizations of graphs that form either |
1252 | | // an upright or reverse tree structure. |
1253 | | |
1254 | | // Construct a direction, RNG seed, and an arbitrary graph from the fuzz input. |
1255 | 0 | SpanReader reader(buffer); |
1256 | 0 | uint64_t rng_seed{0}; |
1257 | 0 | DepGraph<TestBitSet> depgraph_gen; |
1258 | 0 | uint8_t direction{0}; |
1259 | 0 | try { |
1260 | 0 | reader >> direction >> rng_seed >> Using<DepGraphFormatter>(depgraph_gen); |
1261 | 0 | } catch (const std::ios_base::failure&) {} |
1262 | | |
1263 | | // Now construct a new graph, copying the nodes, but leaving only the first parent (even |
1264 | | // direction) or the first child (odd direction). |
1265 | 0 | DepGraph<TestBitSet> depgraph_tree; |
1266 | 0 | for (DepGraphIndex i = 0; i < depgraph_gen.PositionRange(); ++i) { |
1267 | 0 | if (depgraph_gen.Positions()[i]) { |
1268 | 0 | depgraph_tree.AddTransaction(depgraph_gen.FeeRate(i)); |
1269 | 0 | } else { |
1270 | | // For holes, add a dummy transaction which is deleted below, so that non-hole |
1271 | | // transactions retain their position. |
1272 | 0 | depgraph_tree.AddTransaction(FeeFrac{}); |
1273 | 0 | } |
1274 | 0 | } |
1275 | 0 | depgraph_tree.RemoveTransactions(TestBitSet::Fill(depgraph_gen.PositionRange()) - depgraph_gen.Positions()); |
1276 | |
|
1277 | 0 | if (direction & 1) { |
1278 | 0 | for (DepGraphIndex i = 0; i < depgraph_gen.TxCount(); ++i) { |
1279 | 0 | auto children = depgraph_gen.GetReducedChildren(i); |
1280 | 0 | if (children.Any()) { |
1281 | 0 | depgraph_tree.AddDependencies(TestBitSet::Singleton(i), children.First()); |
1282 | 0 | } |
1283 | 0 | } |
1284 | 0 | } else { |
1285 | 0 | for (DepGraphIndex i = 0; i < depgraph_gen.TxCount(); ++i) { |
1286 | 0 | auto parents = depgraph_gen.GetReducedParents(i); |
1287 | 0 | if (parents.Any()) { |
1288 | 0 | depgraph_tree.AddDependencies(TestBitSet::Singleton(parents.First()), i); |
1289 | 0 | } |
1290 | 0 | } |
1291 | 0 | } |
1292 | | |
1293 | | // Retrieve a linearization from the fuzz input. |
1294 | 0 | std::vector<DepGraphIndex> linearization; |
1295 | 0 | linearization = ReadLinearization(depgraph_tree, reader); |
1296 | 0 | SanityCheck(depgraph_tree, linearization); |
1297 | | |
1298 | | // Produce a postlinearized version. |
1299 | 0 | auto post_linearization = linearization; |
1300 | 0 | PostLinearize(depgraph_tree, post_linearization); |
1301 | 0 | SanityCheck(depgraph_tree, post_linearization); |
1302 | | |
1303 | | // Compare diagrams. |
1304 | 0 | auto chunking = ChunkLinearization(depgraph_tree, linearization); |
1305 | 0 | auto post_chunking = ChunkLinearization(depgraph_tree, post_linearization); |
1306 | 0 | auto cmp = CompareChunks(post_chunking, chunking); |
1307 | 0 | assert(cmp >= 0); |
1308 | | |
1309 | | // Verify that post-linearizing again does not change the diagram. The result must be identical |
1310 | | // as post_linearization ought to be optimal already with a tree-structured graph. |
1311 | 0 | auto post_post_linearization = post_linearization; |
1312 | 0 | PostLinearize(depgraph_tree, post_linearization); |
1313 | 0 | SanityCheck(depgraph_tree, post_linearization); |
1314 | 0 | auto post_post_chunking = ChunkLinearization(depgraph_tree, post_post_linearization); |
1315 | 0 | auto cmp_post = CompareChunks(post_post_chunking, post_chunking); |
1316 | 0 | assert(cmp_post == 0); |
1317 | | |
1318 | | // Try to find an even better linearization directly. This must not change the diagram for the |
1319 | | // same reason. |
1320 | 0 | auto [opt_linearization, _optimal, _cost] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization); |
1321 | 0 | auto opt_chunking = ChunkLinearization(depgraph_tree, opt_linearization); |
1322 | 0 | auto cmp_opt = CompareChunks(opt_chunking, post_chunking); |
1323 | 0 | assert(cmp_opt == 0); |
1324 | 0 | } |
1325 | | |
1326 | | FUZZ_TARGET(clusterlin_postlinearize_moved_leaf) |
1327 | 0 | { |
1328 | | // Verify that taking an existing linearization, and moving a leaf to the back, potentially |
1329 | | // increasing its fee, and then post-linearizing, results in something as good as the |
1330 | | // original. This guarantees that in an RBF that replaces a transaction with one of the same |
1331 | | // size but higher fee, applying the "remove conflicts, append new transaction, postlinearize" |
1332 | | // process will never worsen linearization quality. |
1333 | | |
1334 | | // Construct an arbitrary graph and a fee from the fuzz input. |
1335 | 0 | SpanReader reader(buffer); |
1336 | 0 | DepGraph<TestBitSet> depgraph; |
1337 | 0 | int32_t fee_inc{0}; |
1338 | 0 | try { |
1339 | 0 | uint64_t fee_inc_code; |
1340 | 0 | reader >> Using<DepGraphFormatter>(depgraph) >> VARINT(fee_inc_code); |
1341 | 0 | fee_inc = fee_inc_code & 0x3ffff; |
1342 | 0 | } catch (const std::ios_base::failure&) {} |
1343 | 0 | if (depgraph.TxCount() == 0) return; |
1344 | | |
1345 | | // Retrieve two linearizations from the fuzz input. |
1346 | 0 | auto lin = ReadLinearization(depgraph, reader); |
1347 | 0 | auto lin_leaf = ReadLinearization(depgraph, reader); |
1348 | | |
1349 | | // Construct a linearization identical to lin, but with the tail end of lin_leaf moved to the |
1350 | | // back. |
1351 | 0 | std::vector<DepGraphIndex> lin_moved; |
1352 | 0 | for (auto i : lin) { |
1353 | 0 | if (i != lin_leaf.back()) lin_moved.push_back(i); |
1354 | 0 | } |
1355 | 0 | lin_moved.push_back(lin_leaf.back()); |
1356 | | |
1357 | | // Postlinearize lin_moved. |
1358 | 0 | PostLinearize(depgraph, lin_moved); |
1359 | 0 | SanityCheck(depgraph, lin_moved); |
1360 | | |
1361 | | // Compare diagrams (applying the fee delta after computing the old one). |
1362 | 0 | auto old_chunking = ChunkLinearization(depgraph, lin); |
1363 | 0 | depgraph.FeeRate(lin_leaf.back()).fee += fee_inc; |
1364 | 0 | auto new_chunking = ChunkLinearization(depgraph, lin_moved); |
1365 | 0 | auto cmp = CompareChunks(new_chunking, old_chunking); |
1366 | 0 | assert(cmp >= 0); |
1367 | 0 | } |
1368 | | |
1369 | | FUZZ_TARGET(clusterlin_merge) |
1370 | 0 | { |
1371 | | // Construct an arbitrary graph from the fuzz input. |
1372 | 0 | SpanReader reader(buffer); |
1373 | 0 | DepGraph<TestBitSet> depgraph; |
1374 | 0 | try { |
1375 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
1376 | 0 | } catch (const std::ios_base::failure&) {} |
1377 | | |
1378 | | // Retrieve two linearizations from the fuzz input. |
1379 | 0 | auto lin1 = ReadLinearization(depgraph, reader); |
1380 | 0 | auto lin2 = ReadLinearization(depgraph, reader); |
1381 | | |
1382 | | // Merge the two. |
1383 | 0 | auto lin_merged = MergeLinearizations(depgraph, lin1, lin2); |
1384 | | |
1385 | | // Compute chunkings and compare. |
1386 | 0 | auto chunking1 = ChunkLinearization(depgraph, lin1); |
1387 | 0 | auto chunking2 = ChunkLinearization(depgraph, lin2); |
1388 | 0 | auto chunking_merged = ChunkLinearization(depgraph, lin_merged); |
1389 | 0 | auto cmp1 = CompareChunks(chunking_merged, chunking1); |
1390 | 0 | assert(cmp1 >= 0); |
1391 | 0 | auto cmp2 = CompareChunks(chunking_merged, chunking2); |
1392 | 0 | assert(cmp2 >= 0); |
1393 | 0 | } |
1394 | | |
1395 | | FUZZ_TARGET(clusterlin_fix_linearization) |
1396 | 0 | { |
1397 | | // Verify expected properties of FixLinearization() on arbitrary linearizations. |
1398 | | |
1399 | | // Retrieve a depgraph from the fuzz input. |
1400 | 0 | SpanReader reader(buffer); |
1401 | 0 | DepGraph<TestBitSet> depgraph; |
1402 | 0 | try { |
1403 | 0 | reader >> Using<DepGraphFormatter>(depgraph); |
1404 | 0 | } catch (const std::ios_base::failure&) {} |
1405 | | |
1406 | | // Construct an arbitrary linearization (not necessarily topological for depgraph). |
1407 | 0 | std::vector<DepGraphIndex> linearization; |
1408 | | /** Which transactions of depgraph are yet to be included in linearization. */ |
1409 | 0 | TestBitSet todo = depgraph.Positions(); |
1410 | 0 | while (todo.Any()) { |
1411 | | // Read a number from the fuzz input in range [0, todo.Count()). |
1412 | 0 | uint64_t val{0}; |
1413 | 0 | try { |
1414 | 0 | reader >> VARINT(val); |
1415 | 0 | } catch (const std::ios_base::failure&) {} |
1416 | 0 | val %= todo.Count(); |
1417 | | // Find the val'th element in todo, remove it from todo, and append it to linearization. |
1418 | 0 | for (auto idx : todo) { |
1419 | 0 | if (val == 0) { |
1420 | 0 | linearization.push_back(idx); |
1421 | 0 | todo.Reset(idx); |
1422 | 0 | break; |
1423 | 0 | } |
1424 | 0 | --val; |
1425 | 0 | } |
1426 | 0 | } |
1427 | 0 | assert(linearization.size() == depgraph.TxCount()); |
1428 | | |
1429 | | // Determine what prefix of linearization is topological, i.e., the position of the first entry |
1430 | | // in linearization which corresponds to a transaction that is not preceded by all its |
1431 | | // ancestors. |
1432 | 0 | size_t topo_prefix = 0; |
1433 | 0 | todo = depgraph.Positions(); |
1434 | 0 | while (topo_prefix < linearization.size()) { |
1435 | 0 | DepGraphIndex idx = linearization[topo_prefix]; |
1436 | 0 | todo.Reset(idx); |
1437 | 0 | if (todo.Overlaps(depgraph.Ancestors(idx))) break; |
1438 | 0 | ++topo_prefix; |
1439 | 0 | } |
1440 | | |
1441 | | // Then make a fixed copy of linearization. |
1442 | 0 | auto linearization_fixed = linearization; |
1443 | 0 | FixLinearization(depgraph, linearization_fixed); |
1444 | | // Sanity check it (which includes testing whether it is topological). |
1445 | 0 | SanityCheck(depgraph, linearization_fixed); |
1446 | | |
1447 | | // FixLinearization does not modify the topological prefix of linearization. |
1448 | 0 | assert(std::equal(linearization.begin(), linearization.begin() + topo_prefix, |
1449 | 0 | linearization_fixed.begin())); |
1450 | | // This also means that if linearization was entirely topological, FixLinearization cannot have |
1451 | | // modified it. This is implied by the assertion above already, but repeat it explicitly. |
1452 | 0 | if (topo_prefix == linearization.size()) { |
1453 | 0 | assert(linearization == linearization_fixed); |
1454 | 0 | } |
1455 | 0 | } |