txgraph: Add ability to configure maximum cluster size/weight (feature)

This is integrated with the oversized property: the graph is oversized when
any connected component within it contains more than the cluster count limit
many transactions, or when their combined size/weight exceeds the cluster size
limit.

It becomes disallowed to call AddTransaction with a size larger than this limit.
In addition, SetTransactionFeeRate becomes SetTransactionFee, so that we do not
need to deal with the case that a call to this function might affect the
oversizedness.
This commit is contained in:
Pieter Wuille 2024-12-17 08:13:25 -05:00
parent a0becaaa9c
commit 7e869040fe
3 changed files with 55 additions and 17 deletions

View file

@ -56,9 +56,12 @@ struct SimTxGraph
/** Which transactions have been modified in the graph since creation, either directly or by /** Which transactions have been modified in the graph since creation, either directly or by
* being in a cluster which includes modifications. Only relevant for the staging graph. */ * being in a cluster which includes modifications. Only relevant for the staging graph. */
SetType modified; SetType modified;
/** The configured maximum total size of transactions per cluster. */
uint64_t max_cluster_size;
/** Construct a new SimTxGraph with the specified maximum cluster count. */ /** Construct a new SimTxGraph with the specified maximum cluster count. */
explicit SimTxGraph(DepGraphIndex max_cluster) : max_cluster_count(max_cluster) {} explicit SimTxGraph(DepGraphIndex max_cluster, uint64_t max_size) :
max_cluster_count(max_cluster), max_cluster_size(max_size) {}
// Permit copying and moving. // Permit copying and moving.
SimTxGraph(const SimTxGraph&) noexcept = default; SimTxGraph(const SimTxGraph&) noexcept = default;
@ -78,6 +81,9 @@ struct SimTxGraph
while (todo.Any()) { while (todo.Any()) {
auto component = graph.FindConnectedComponent(todo); auto component = graph.FindConnectedComponent(todo);
if (component.Count() > max_cluster_count) oversized = true; if (component.Count() > max_cluster_count) oversized = true;
uint64_t component_size{0};
for (auto i : component) component_size += graph.FeeRate(i).size;
if (component_size > max_cluster_size) oversized = true;
todo -= component; todo -= component;
} }
} }
@ -262,12 +268,16 @@ FUZZ_TARGET(txgraph)
// Decide the maximum number of transactions per cluster we will use in this simulation. // Decide the maximum number of transactions per cluster we will use in this simulation.
auto max_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT); auto max_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
// And the maximum combined size of transactions per cluster.
auto max_size = provider.ConsumeIntegralInRange<uint64_t>(1, 0x3fffff * MAX_CLUSTER_COUNT_LIMIT);
// Maximum individual transaction size.
auto max_tx_size = std::min<uint64_t>(0x3fffff, max_size);
// Construct a real graph, and a vector of simulated graphs (main, and possibly staging). // Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
auto real = MakeTxGraph(max_count); auto real = MakeTxGraph(max_count, max_size);
std::vector<SimTxGraph> sims; std::vector<SimTxGraph> sims;
sims.reserve(2); sims.reserve(2);
sims.emplace_back(max_count); sims.emplace_back(max_count, max_size);
/** Struct encapsulating information about a BlockBuilder that's currently live. */ /** Struct encapsulating information about a BlockBuilder that's currently live. */
struct BlockBuilderData struct BlockBuilderData
@ -391,12 +401,12 @@ FUZZ_TARGET(txgraph)
if (alt) { if (alt) {
// If alt is true, pick fee and size from the entire range. // If alt is true, pick fee and size from the entire range.
fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff); fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff); size = provider.ConsumeIntegralInRange<int32_t>(1, max_tx_size);
} else { } else {
// Otherwise, use smaller range which consume fewer fuzz input bytes, as just // Otherwise, use smaller range which consume fewer fuzz input bytes, as just
// these are likely sufficient to trigger all interesting code paths already. // these are likely sufficient to trigger all interesting code paths already.
fee = provider.ConsumeIntegral<uint8_t>(); fee = provider.ConsumeIntegral<uint8_t>();
size = provider.ConsumeIntegral<uint8_t>() + 1; size = provider.ConsumeIntegralInRange<uint32_t>(1, std::min<uint32_t>(0xff, max_tx_size));
} }
FeePerWeight feerate{fee, size}; FeePerWeight feerate{fee, size};
// Create a real TxGraph::Ref. // Create a real TxGraph::Ref.
@ -570,13 +580,17 @@ FUZZ_TARGET(txgraph)
assert(result.size() <= max_count); assert(result.size() <= max_count);
// Require the result to be topologically valid and not contain duplicates. // Require the result to be topologically valid and not contain duplicates.
auto left = sel_sim.graph.Positions(); auto left = sel_sim.graph.Positions();
uint64_t total_size{0};
for (auto refptr : result) { for (auto refptr : result) {
auto simpos = sel_sim.Find(refptr); auto simpos = sel_sim.Find(refptr);
total_size += sel_sim.graph.FeeRate(simpos).size;
assert(simpos != SimTxGraph::MISSING); assert(simpos != SimTxGraph::MISSING);
assert(left[simpos]); assert(left[simpos]);
left.Reset(simpos); left.Reset(simpos);
assert(!sel_sim.graph.Ancestors(simpos).Overlaps(left)); assert(!sel_sim.graph.Ancestors(simpos).Overlaps(left));
} }
// Check cluster size limit.
assert(total_size <= max_size);
// Require the set to be connected. // Require the set to be connected.
auto result_set = sel_sim.MakeSet(result); auto result_set = sel_sim.MakeSet(result);
assert(sel_sim.graph.IsConnected(result_set)); assert(sel_sim.graph.IsConnected(result_set));
@ -956,13 +970,17 @@ FUZZ_TARGET(txgraph)
// linearization). // linearization).
std::vector<DepGraphIndex> simlin; std::vector<DepGraphIndex> simlin;
SimTxGraph::SetType done; SimTxGraph::SetType done;
uint64_t total_size{0};
for (TxGraph::Ref* ptr : cluster) { for (TxGraph::Ref* ptr : cluster) {
auto simpos = sim.Find(ptr); auto simpos = sim.Find(ptr);
assert(sim.graph.Descendants(simpos).IsSubsetOf(component - done)); assert(sim.graph.Descendants(simpos).IsSubsetOf(component - done));
done.Set(simpos); done.Set(simpos);
assert(sim.graph.Ancestors(simpos).IsSubsetOf(done)); assert(sim.graph.Ancestors(simpos).IsSubsetOf(done));
simlin.push_back(simpos); simlin.push_back(simpos);
total_size += sim.graph.FeeRate(simpos).size;
} }
// Check cluster size.
assert(total_size <= max_size);
// Construct a chunking object for the simulated graph, using the reported cluster // Construct a chunking object for the simulated graph, using the reported cluster
// linearization as ordering, and compare it against the reported chunk feerates. // linearization as ordering, and compare it against the reported chunk feerates.
if (sims.size() == 1 || main_only) { if (sims.size() == 1 || main_only) {

View file

@ -109,6 +109,8 @@ public:
} }
/** Get the number of transactions in this Cluster. */ /** Get the number of transactions in this Cluster. */
LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); } LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); }
/** Get the total size of the transactions in this Cluster. */
uint64_t GetTxSize() const noexcept;
/** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */ /** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */
GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; } GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; }
/** Only called by Graph::SwapIndexes. */ /** Only called by Graph::SwapIndexes. */
@ -199,6 +201,8 @@ private:
FastRandomContext m_rng; FastRandomContext m_rng;
/** This TxGraphImpl's maximum cluster count limit. */ /** This TxGraphImpl's maximum cluster count limit. */
const DepGraphIndex m_max_cluster_count; const DepGraphIndex m_max_cluster_count;
/** This TxGraphImpl's maximum cluster size limit. */
const uint64_t m_max_cluster_size;
/** Information about one group of Clusters to be merged. */ /** Information about one group of Clusters to be merged. */
struct GroupEntry struct GroupEntry
@ -391,9 +395,10 @@ private:
std::vector<GraphIndex> m_unlinked; std::vector<GraphIndex> m_unlinked;
public: public:
/** Construct a new TxGraphImpl with the specified maximum cluster count. */ /** Construct a new TxGraphImpl with the specified limits. */
explicit TxGraphImpl(DepGraphIndex max_cluster_count) noexcept : explicit TxGraphImpl(DepGraphIndex max_cluster_count, uint64_t max_cluster_size) noexcept :
m_max_cluster_count(max_cluster_count), m_max_cluster_count(max_cluster_count),
m_max_cluster_size(max_cluster_size),
m_main_chunkindex(ChunkOrder(this)) m_main_chunkindex(ChunkOrder(this))
{ {
Assume(max_cluster_count >= 1); Assume(max_cluster_count >= 1);
@ -624,6 +629,15 @@ void TxGraphImpl::CreateChunkData(GraphIndex idx, LinearizationIndex chunk_count
} }
} }
uint64_t Cluster::GetTxSize() const noexcept
{
uint64_t ret{0};
for (auto i : m_linearization) {
ret += m_depgraph.FeeRate(i).size;
}
return ret;
}
void TxGraphImpl::ClearLocator(int level, GraphIndex idx) noexcept void TxGraphImpl::ClearLocator(int level, GraphIndex idx) noexcept
{ {
auto& entry = m_entries[idx]; auto& entry = m_entries[idx];
@ -1428,10 +1442,12 @@ void TxGraphImpl::GroupClusters(int level) noexcept
new_entry.m_deps_offset = clusterset.m_deps_to_add.size(); new_entry.m_deps_offset = clusterset.m_deps_to_add.size();
new_entry.m_deps_count = 0; new_entry.m_deps_count = 0;
uint32_t total_count{0}; uint32_t total_count{0};
uint64_t total_size{0};
// Add all its clusters to it (copying those from an_clusters to m_group_clusters). // Add all its clusters to it (copying those from an_clusters to m_group_clusters).
while (an_clusters_it != an_clusters.end() && an_clusters_it->second == rep) { while (an_clusters_it != an_clusters.end() && an_clusters_it->second == rep) {
clusterset.m_group_data->m_group_clusters.push_back(an_clusters_it->first); clusterset.m_group_data->m_group_clusters.push_back(an_clusters_it->first);
total_count += an_clusters_it->first->GetTxCount(); total_count += an_clusters_it->first->GetTxCount();
total_size += an_clusters_it->first->GetTxSize();
++an_clusters_it; ++an_clusters_it;
++new_entry.m_cluster_count; ++new_entry.m_cluster_count;
} }
@ -1442,7 +1458,7 @@ void TxGraphImpl::GroupClusters(int level) noexcept
++new_entry.m_deps_count; ++new_entry.m_deps_count;
} }
// Detect oversizedness. // Detect oversizedness.
if (total_count > m_max_cluster_count) { if (total_count > m_max_cluster_count || total_size > m_max_cluster_size) {
clusterset.m_group_data->m_group_oversized = true; clusterset.m_group_data->m_group_oversized = true;
} }
} }
@ -1576,6 +1592,7 @@ Cluster::Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feer
TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept
{ {
Assume(m_main_chunkindex_observers == 0 || GetTopLevel() != 0); Assume(m_main_chunkindex_observers == 0 || GetTopLevel() != 0);
Assume(feerate.size > 0 && uint64_t(feerate.size) <= m_max_cluster_size);
// Construct a new Ref. // Construct a new Ref.
Ref ret; Ref ret;
// Construct a new Entry, and link it with the Ref. // Construct a new Entry, and link it with the Ref.
@ -2121,6 +2138,8 @@ void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const
assert(m_linearization.size() <= graph.m_max_cluster_count); assert(m_linearization.size() <= graph.m_max_cluster_count);
// The level must match the level the Cluster occurs in. // The level must match the level the Cluster occurs in.
assert(m_level == level); assert(m_level == level);
// The sum of their sizes cannot exceed m_max_cluster_size.
assert(GetTxSize() <= graph.m_max_cluster_size);
// m_quality and m_setindex are checked in TxGraphImpl::SanityCheck. // m_quality and m_setindex are checked in TxGraphImpl::SanityCheck.
// Compute the chunking of m_linearization. // Compute the chunking of m_linearization.
@ -2498,7 +2517,7 @@ TxGraph::Ref::Ref(Ref&& other) noexcept
std::swap(m_index, other.m_index); std::swap(m_index, other.m_index);
} }
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count) noexcept std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size) noexcept
{ {
return std::make_unique<TxGraphImpl>(max_cluster_count); return std::make_unique<TxGraphImpl>(max_cluster_count, max_cluster_size);
} }

View file

@ -63,10 +63,10 @@ public:
/** Virtual destructor, so inheriting is safe. */ /** Virtual destructor, so inheriting is safe. */
virtual ~TxGraph() = default; virtual ~TxGraph() = default;
/** Construct a new transaction with the specified feerate, and return a Ref to it. /** Construct a new transaction with the specified feerate, and return a Ref to it.
* If a staging graph exists, the new transaction is only created there. In all * If a staging graph exists, the new transaction is only created there. feerate.size cannot
* further calls, only Refs created by AddTransaction() are allowed to be passed to this * exceed the graph's max cluster size. In all further calls, only Refs created by
* TxGraph object (or empty Ref objects). Ref objects may outlive the TxGraph they were * AddTransaction() are allowed to be passed to this TxGraph object (or empty Ref objects).
* created for. */ * Ref objects may outlive the TxGraph they were created for. */
[[nodiscard]] virtual Ref AddTransaction(const FeePerWeight& feerate) noexcept = 0; [[nodiscard]] virtual Ref AddTransaction(const FeePerWeight& feerate) noexcept = 0;
/** Remove the specified transaction. If a staging graph exists, the removal only happens /** Remove the specified transaction. If a staging graph exists, the removal only happens
* there. This is a no-op if the transaction was already removed. * there. This is a no-op if the transaction was already removed.
@ -240,8 +240,9 @@ public:
}; };
}; };
/** Construct a new TxGraph with the specified limit on transactions within a cluster. That /** Construct a new TxGraph with the specified limit on transactions within a cluster, and the
* number cannot exceed MAX_CLUSTER_COUNT_LIMIT. */ * specified limit on the sum of transaction sizes within a cluster. max_cluster_count cannot
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count) noexcept; * exceed MAX_CLUSTER_COUNT_LIMIT. */
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size) noexcept;
#endif // BITCOIN_TXGRAPH_H #endif // BITCOIN_TXGRAPH_H