|
5 | 5 | #ifndef BITCOIN_CLUSTER_LINEARIZE_H
|
6 | 6 | #define BITCOIN_CLUSTER_LINEARIZE_H
|
7 | 7 |
|
| 8 | +#include <algorithm> |
| 9 | +#include <numeric> |
8 | 10 | #include <optional>
|
9 | 11 | #include <stdint.h>
|
10 | 12 | #include <vector>
|
@@ -176,13 +178,23 @@ struct SetInfo
|
176 | 178 | /** Their combined fee and size. */
|
177 | 179 | FeeFrac feerate;
|
178 | 180 |
|
| 181 | + /** Construct a SetInfo for the empty set. */ |
| 182 | + SetInfo() noexcept = default; |
| 183 | + |
179 | 184 | /** Construct a SetInfo for a specified set and feerate. */
|
180 | 185 | SetInfo(const SetType& txn, const FeeFrac& fr) noexcept : transactions(txn), feerate(fr) {}
|
181 | 186 |
|
182 | 187 | /** Construct a SetInfo for a set of transactions in a depgraph. */
|
183 | 188 | explicit SetInfo(const DepGraph<SetType>& depgraph, const SetType& txn) noexcept :
|
184 | 189 | transactions(txn), feerate(depgraph.FeeRate(txn)) {}
|
185 | 190 |
|
| 191 | + /** Construct a new SetInfo equal to this, with more transactions added (which may overlap |
| 192 | + * with the existing transactions in the SetInfo). */ |
| 193 | + [[nodiscard]] SetInfo Add(const DepGraph<SetType>& depgraph, const SetType& txn) const noexcept |
| 194 | + { |
| 195 | + return {transactions | txn, feerate + depgraph.FeeRate(txn - transactions)}; |
| 196 | + } |
| 197 | + |
186 | 198 | /** Permit equality testing. */
|
187 | 199 | friend bool operator==(const SetInfo&, const SetInfo&) noexcept = default;
|
188 | 200 | };
|
@@ -283,6 +295,165 @@ class AncestorCandidateFinder
|
283 | 295 | }
|
284 | 296 | };
|
285 | 297 |
|
| 298 | +/** Class encapsulating the state needed to perform search for good candidate sets. |
| 299 | + * |
| 300 | + * It is initialized for an entire DepGraph, and parts of the graph can be dropped by calling |
| 301 | + * MarkDone(). |
| 302 | + * |
| 303 | + * As long as any part of the graph remains, FindCandidateSet() can be called to perform a search |
| 304 | + * over the set of topologically-valid subsets of that remainder, with a limit on how many |
| 305 | + * combinations are tried. |
| 306 | + */ |
| 307 | +template<typename SetType> |
| 308 | +class SearchCandidateFinder |
| 309 | +{ |
| 310 | + /** Internal dependency graph for the cluster. */ |
| 311 | + const DepGraph<SetType>& m_depgraph; |
| 312 | + /** Which transactions are left to do (sorted indices). */ |
| 313 | + SetType m_todo; |
| 314 | + |
| 315 | +public: |
| 316 | + /** Construct a candidate finder for a graph. |
| 317 | + * |
| 318 | + * @param[in] depgraph Dependency graph for the to-be-linearized cluster. |
| 319 | + * |
| 320 | + * Complexity: O(1). |
| 321 | + */ |
| 322 | + SearchCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept : |
| 323 | + m_depgraph(depgraph), |
| 324 | + m_todo(SetType::Fill(depgraph.TxCount())) {} |
| 325 | + |
| 326 | + /** Check whether any unlinearized transactions remain. */ |
| 327 | + bool AllDone() const noexcept |
| 328 | + { |
| 329 | + return m_todo.None(); |
| 330 | + } |
| 331 | + |
| 332 | + /** Find a high-feerate topologically-valid subset of what remains of the cluster. |
| 333 | + * Requires !AllDone(). |
| 334 | + * |
| 335 | + * @param[in] max_iterations The maximum number of optimization steps that will be performed. |
| 336 | + * @param[in] best A set/feerate pair with an already-known good candidate. This may |
| 337 | + * be empty. |
| 338 | + * @return A pair of: |
| 339 | + * - The best (highest feerate, smallest size as tiebreaker) |
| 340 | + * topologically valid subset (and its feerate) that was |
| 341 | + * encountered during search. It will be at least as good as the |
| 342 | + * best passed in (if not empty). |
| 343 | + * - The number of optimization steps that were performed. This will |
| 344 | + * be <= max_iterations. If strictly < max_iterations, the |
| 345 | + * returned subset is optimal. |
| 346 | + * |
| 347 | + * Complexity: O(N * min(max_iterations, 2^N)) where N=depgraph.TxCount(). |
| 348 | + */ |
| 349 | + std::pair<SetInfo<SetType>, uint64_t> FindCandidateSet(uint64_t max_iterations, SetInfo<SetType> best) noexcept |
| 350 | + { |
| 351 | + Assume(!AllDone()); |
| 352 | + |
| 353 | + /** Type for work queue items. */ |
| 354 | + struct WorkItem |
| 355 | + { |
| 356 | + /** Set of transactions definitely included (and its feerate). This must be a subset |
| 357 | + * of m_todo, and be topologically valid (includes all in-m_todo ancestors of |
| 358 | + * itself). */ |
| 359 | + SetInfo<SetType> inc; |
| 360 | + /** Set of undecided transactions. This must be a subset of m_todo, and have no overlap |
| 361 | + * with inc. The set (inc | und) must be topologically valid. */ |
| 362 | + SetType und; |
| 363 | + |
| 364 | + /** Construct a new work item. */ |
| 365 | + WorkItem(SetInfo<SetType>&& i, SetType&& u) noexcept : |
| 366 | + inc(std::move(i)), und(std::move(u)) {} |
| 367 | + }; |
| 368 | + |
| 369 | + /** The queue of work items. */ |
| 370 | + std::vector<WorkItem> queue; |
| 371 | + |
| 372 | + // Create an initial entry with m_todo as undecided. Also use it as best if not provided, |
| 373 | + // so that during the work processing loop below, and during the add_fn/split_fn calls, we |
| 374 | + // do not need to deal with the best=empty case. |
| 375 | + if (best.feerate.IsEmpty()) best = SetInfo(m_depgraph, m_todo); |
| 376 | + queue.emplace_back(SetInfo<SetType>{}, SetType{m_todo}); |
| 377 | + |
| 378 | + /** Local copy of the iteration limit. */ |
| 379 | + uint64_t iterations_left = max_iterations; |
| 380 | + |
| 381 | + /** Internal function to add an item to the queue of elements to explore if there are any |
| 382 | + * transactions left to split on, and to update best. |
| 383 | + * |
| 384 | + * - inc: the "inc" value for the new work item (must be topological). |
| 385 | + * - und: the "und" value for the new work item ((inc | und) must be topological). |
| 386 | + */ |
| 387 | + auto add_fn = [&](SetInfo<SetType> inc, SetType und) noexcept { |
| 388 | + if (!inc.feerate.IsEmpty()) { |
| 389 | + // If inc's feerate is better than best's, remember it as our new best. |
| 390 | + if (inc.feerate > best.feerate) { |
| 391 | + best = inc; |
| 392 | + } |
| 393 | + } else { |
| 394 | + Assume(inc.transactions.None()); |
| 395 | + } |
| 396 | + |
| 397 | + // Make sure there are undecided transactions left to split on. |
| 398 | + if (und.None()) return; |
| 399 | + |
| 400 | + // Actually construct a new work item on the queue. |
| 401 | + queue.emplace_back(std::move(inc), std::move(und)); |
| 402 | + }; |
| 403 | + |
| 404 | + /** Internal process function. It takes an existing work item, and splits it in two: one |
| 405 | + * with a particular transaction (and its ancestors) included, and one with that |
| 406 | + * transaction (and its descendants) excluded. */ |
| 407 | + auto split_fn = [&](WorkItem&& elem) noexcept { |
| 408 | + // Any queue element must have undecided transactions left, otherwise there is nothing |
| 409 | + // to explore anymore. |
| 410 | + Assume(elem.und.Any()); |
| 411 | + // The included and undecided set are all subsets of m_todo. |
| 412 | + Assume(elem.inc.transactions.IsSubsetOf(m_todo) && elem.und.IsSubsetOf(m_todo)); |
| 413 | + // Included transactions cannot be undecided. |
| 414 | + Assume(!elem.inc.transactions.Overlaps(elem.und)); |
| 415 | + |
| 416 | + // Pick the first undecided transaction as the one to split on. |
| 417 | + const ClusterIndex split = elem.und.First(); |
| 418 | + |
| 419 | + // Add a work item corresponding to exclusion of the split transaction. |
| 420 | + const auto& desc = m_depgraph.Descendants(split); |
| 421 | + add_fn(/*inc=*/elem.inc, |
| 422 | + /*und=*/elem.und - desc); |
| 423 | + |
| 424 | + // Add a work item corresponding to inclusion of the split transaction. |
| 425 | + const auto anc = m_depgraph.Ancestors(split) & m_todo; |
| 426 | + add_fn(/*inc=*/elem.inc.Add(m_depgraph, anc), |
| 427 | + /*und=*/elem.und - anc); |
| 428 | + |
| 429 | + // Account for the performed split. |
| 430 | + --iterations_left; |
| 431 | + }; |
| 432 | + |
| 433 | + // Work processing loop. |
| 434 | + while (!queue.empty()) { |
| 435 | + if (!iterations_left) break; |
| 436 | + auto elem = queue.back(); |
| 437 | + queue.pop_back(); |
| 438 | + split_fn(std::move(elem)); |
| 439 | + } |
| 440 | + |
| 441 | + // Return the found best set and the number of iterations performed. |
| 442 | + return {std::move(best), max_iterations - iterations_left}; |
| 443 | + } |
| 444 | + |
| 445 | + /** Remove a subset of transactions from the cluster being linearized. |
| 446 | + * |
| 447 | + * Complexity: O(N) where N=done.Count(). |
| 448 | + */ |
| 449 | + void MarkDone(const SetType& done) noexcept |
| 450 | + { |
| 451 | + Assume(done.Any()); |
| 452 | + Assume(done.IsSubsetOf(m_todo)); |
| 453 | + m_todo -= done; |
| 454 | + } |
| 455 | +}; |
| 456 | + |
286 | 457 | } // namespace cluster_linearize
|
287 | 458 |
|
288 | 459 | #endif // BITCOIN_CLUSTER_LINEARIZE_H
|
0 commit comments