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