cluster mempool: introduce TxGraph#31363
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31363. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
1e432ca to
36a9db4
Compare
84fcb24 to
a93de3b
Compare
c4b4e1f to
18184f4
Compare
|
Concept ACK |
|
I'm aware this is a big chunk of code, but perhaps this can aid review a bit. The biggest commits is txgraph: (feature) add initial version. To get an idea of what is going on, I think it's useful to look (in order) at:
|
|
I have pushed a substantial change to the |
ismaelsadeeq
left a comment
There was a problem hiding this comment.
Concept ACK
GetChunkFeerate (get the mining score of a transaction)
From reading the description and skimming through the code, IIUC TxGraph encapsulates knowledge about fees and sizes but lacks knowledge about prioritization. This implies that the fee it uses is the effective fee, not the modified fee that includes prioritization.
How are we accounting for the real mining score of a Ref without that prioritization knowledge? The current mining algorithm uses the modified ancestor fee to calculate the mining score.
Could you clarify how prioritization will be handled post-cluster mempool?
|
@ismaelsadeeq TxGraph (and DepGraph, and FeeFrac) just treat "fee" and "size" as numbers whose ratio is to be maximized. These classes don't care what meaning they correspond to. In practice, I expect that the mempool code will provide the modified fee as "fee", and vsize or weight as "size". |
|
Thank you for clarifying. Reading 'lacks knowledge about prioritization' led me to infer that. |
|
A few changes:
|
This adds a function to query the chunk feerate of a transaction, by caching it inside the Entry objects.
…ature) Instead of leaving the responsibility on higher layers to guarantee that no connected component within TxGraph (a barely exposed concept, except through GetCluster()) exceeds the cluster count limit, move this responsibility to TxGraph itself: * TxGraph retains a cluster count limit, but it becomes configurable at construction time (this primarily helps with testing that it is properly enforced). * It is always allowed to perform mutators on TxGraph, even if they would cause the cluster count limit to be exceeded. Instead, TxGraph exposes an IsOversized() function, which queries whether it is in a special "oversize" state. * During oversize state, many inspectors are unavailable, but mutators remain valid, so the higher layer can "fix" the oversize state before continuing.
The m_deps_to_add vector is sorted by child Cluster*, which matches the order of an_clusters. This means we can walk through m_deps_to_add while doing the representative lookups for an_clusters, and reuse them.
…tion) Since m_deps_to_add has been sorted by child Cluster* already, all dependencies with the same child will be processed consecutively. Take advantage of this by remember the last partition merged with, and reusing that if applicable.
Chunk-based information (primarily, chunk feerates) are never accessed without first bringing the relevant Clusters to an "acceptable" quality level. Thus, while operations are ongoing and Clusters are not acceptable, we can omit computing the chunkings and chunk feerates for Clusters.
When transactions are removed from the tail of a cluster, we know the existing linearization remains acceptable (if it already was), but may just need splitting and postlinearization, so special case these into separate quality levels.
This is a preparation for a next commit where a TxGraph will start representing potentially two distinct graphs (a main one, and a staging one with proposed changes).
Move a number of related modifications to TxGraphImpl into a separate function for removal of transactions. This is preparation for a later commit where this will be useful in more than one place.
In order to make it easy to evaluate proposed changes to a TxGraph, introduce a "staging" mode, where mutators (AddTransaction, AddDependency, RemoveTransaction) do not modify the actual graph, but just a staging version of it. That staging graph can then be commited (replacing the main one with it), or aborted (discarding the staging).
Before this commit, if a TxGraph::Ref object is destroyed, it becomes impossible to refer to, but the actual corresponding transaction node in the TxGraph remains, and remains indefinitely as there is no way to remove it. Fix this by making the destruction of TxGraph::Ref trigger immediate removal of the corresponding transaction in TxGraph, both in main and staging if it exists.
In order to make it possible for higher layers to compare transaction quality (ordering within the implicit total ordering on the mempool), expose a comparison function and test it.
This can be called when the caller has time to spend now, and wants future operations to be fast.
This is a preparation for the next commit, which adds a feature to request the Refs to multiple ancestors/descendants at once.
Like GetAncestors and GetDescendants, but for the union of multiple inputs.
|
reACK b2ea365 nice doc improvements
|
|
It occurs to me that it may be possible to get rid of Anyway, after looking into it, I don't think it's worth changing at this stage in the PR, but maybe it can be done as a follow-up. |
|
reACK b2ea365 |
glozow
left a comment
There was a problem hiding this comment.
ACK b2ea365
Reviewed the structure to convince myself this approach makes sense to support the features required, and reviewed the SimTxGraph + differential fuzzer in closer detail. I haven't read through all the TxGraphImpl functions but did some manual mutation testing.
| // Update the linearization. | ||
| m_linearization = std::move(linearization); | ||
| // Update the Cluster's quality. | ||
| auto new_quality = optimal ? QualityLevel::OPTIMAL : QualityLevel::ACCEPTABLE; |
There was a problem hiding this comment.
fwiw, I don't think there is test coverage for this (i.e. fine if this is always set to optimal), though I expect any problems would become more obvious when mempool things are hooked up to txgraph
There was a problem hiding this comment.
Right. The difference between OPTIMAL and ACCEPTABLE is not observable as far as the interface is concerned, because it only promises a best effort.
There is an operational difference, though: DoWork() will not bother putting more effort into OPTIMAL clusters.
| while (true) { | ||
| auto old_component = component; | ||
| for (auto i : component) { | ||
| component |= sel_sim.graph.Ancestors(i); | ||
| component |= sel_sim.graph.Descendants(i); | ||
| } | ||
| if (component == old_component) break; | ||
| } |
There was a problem hiding this comment.
Why can't FindConnectedComponent be used instead?
There was a problem hiding this comment.
That finds a connected component, but not the one that this particular transaction is in. I guess it could be refactored to support that too.
| top_sim.AddDependency(par, chl); | ||
| real->AddDependency(*par, *chl); | ||
| break; | ||
| } else if (top_sim.removed.size() < 100 && command-- == 0) { |
There was a problem hiding this comment.
in 05abf33
I suppose this stops us from making removed larger than MAX_TRANSACTIONS + 100?
There was a problem hiding this comment.
Where does the MAX_TRANSACTIONS come from? It should limit the size of removed to 100. This was added when the fuzzer ran for much longer (up to 1000 iterations, I think), and I wanted to avoid it just creating and removing transactions. It currently doesn't do much anymore with the lower iteration count.
There was a problem hiding this comment.
I was thinking that if all the transactions are descended from this tx, you'd add all of them to to_remove here. So if you started out with 99, you can end up with a lot more than 100 afterward. You wouldn't do more removals afterward, but you'd have more than 100.
Part of cluster mempool: #30289.
1. Overview
This introduces the
TxGraphclass, which encapsulates knowledge about the (effective) fees, sizes, and dependencies between all mempool transactions, but nothing else. In particular, it lacks knowledge aboutCTransaction, inputs, outputs, txids, wtxids, prioritization, validatity, policy rules, and a lot more. Being restricted to just those aspects of the mempool makes the behavior very easy to fully specify (ignoring the actual linearizations produced), and write simulation-based tests for (which are included in this PR).2. Interface
The interface can be largely categorized into:
AddTransaction(add a new transaction with specified feerate, and get aRefobject back to identify it).RemoveTransaction(given aRefobject, remove the transaction).AddDependency(given twoRefobjects, add a dependency between them).SetTransactionFee(modify the fee associated with a Ref object).GetAncestors(get the ancestor set in the form ofRef*pointers)GetAncestorsUnion(like above, but for the union of ancestors of multipleRef*pointers)GetDescendants(get the descendant set in the form ofRef*pointers)GetDescendantsUnion(like above, but for the union of ancestors of multipleRef*pointers)GetCluster(get the connected component set in the form ofRef*pointers, in the order they would be mined).GetIndividualFeerate(get the feerate of a transaction)GetChunkFeerate(get the mining score of a transaction)CountDistinctClusters(count the number of distinct clusters a list ofRefs belong to)StartStaging(make all future mutations operate on a proposed transaction graph)CommitStaging(apply all the changes that are staged)AbortStaging(discard all the changes that are staged)DoWork(do queued-up computations now, so that future operations are fast)This
TxGraph::Reftype used as a "handle" on transactions in the graph can be inherited from, and the idea is that in the full cluster mempool implementation (#28676, after it is rebased on this),CTxMempoolEntrywill inherit from it, and all actually used Ref objects will beCTxMempoolEntrys. With that, the mempool code can just cast anyRef*returned by txgraph toCTxMempoolEntry*.3. Implementation
Internally the graph data is kept in clustered form (partitioned into connected components), for which linearizations are maintained and updated as needed using the
cluster_linearize.halgorithms under the hood, but this is hidden from the users of this class. Implementation-wise, mutations are generally applied lazily, appending to queues of to-be-removed transactions and to-be-added dependencies, so they can be batched for higher performance. Inspectors will generally only evaluate as much as is needed to answer queries, with roughly 5 levels of processing to go to fully instantiated and acceptable cluster linearizations, in order:ApplyRemovals(take batches of to-be-removed transactions and translate them to "holes" in the corresponding Clusters/DepGraphs).SplitAll(creating holes in Clusters may cause them to break apart into smaller connected components, so make turn them into separate Clusters/linearizations).GroupClusters(figure out which Clusters will need to be combined in order to add requested to-be-added dependencies, as these may span clusters).ApplyDependencies(actually merge Clusters as precomputed byGroupClusters, and add the dependencies between them).MakeAcceptable(perform the LIMO linearization algorithm on Clusters to make sure their linearizations are acceptable).4. Future work
This is only an initial version of TxGraph, and some functionality is missing before #28676 can be rebased on top of it:
Then there are further improvements possible which would not block other work: