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