Skip to content

Commit 19b14e6

Browse files
committed
txgraph: Permit transactions that exceed cluster size limit (feature)
This removes the restriction added in the previous commit that individual transactions do not exceed the max cluster size limit. With this change, the responsibility for enforcing cluster size limits can be localized purely in TxGraph, without callers (and in particular, tests) needing to duplicate the enforcement for individual transactions.
1 parent c4287b9 commit 19b14e6

File tree

3 files changed

+85
-36
lines changed

3 files changed

+85
-36
lines changed

src/test/fuzz/txgraph.cpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ struct SimTxGraph
134134
simmap[simpos] = std::make_shared<TxGraph::Ref>();
135135
auto ptr = simmap[simpos].get();
136136
simrevmap[ptr] = simpos;
137+
// This may invalidate our cached oversized value.
138+
if (oversized.has_value() && !*oversized) oversized = std::nullopt;
137139
return ptr;
138140
}
139141

@@ -269,12 +271,8 @@ FUZZ_TARGET(txgraph)
269271
/** The maximum number of transactions per (non-oversized) cluster we will use in this
270272
* simulation. */
271273
auto max_cluster_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
272-
/** The maximum total size of transactions in a cluster, which also makes it an upper bound
273-
* on the individual size of a transaction (but this restriction will be lifted in a future
274-
* commit). */
274+
/** The maximum total size of transactions in a (non-oversized) cluster. */
275275
auto max_cluster_size = provider.ConsumeIntegralInRange<uint64_t>(1, 0x3fffff * MAX_CLUSTER_COUNT_LIMIT);
276-
/** The maximum individual transaction size used in this test (not a TxGraph parameter). */
277-
auto max_tx_size = std::min<uint64_t>(0x3fffff, max_cluster_size);
278276

279277
// Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
280278
auto real = MakeTxGraph(max_cluster_count, max_cluster_size);
@@ -404,12 +402,12 @@ FUZZ_TARGET(txgraph)
404402
if (alt) {
405403
// If alt is true, pick fee and size from the entire range.
406404
fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
407-
size = provider.ConsumeIntegralInRange<int32_t>(1, max_tx_size);
405+
size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
408406
} else {
409407
// Otherwise, use smaller range which consume fewer fuzz input bytes, as just
410408
// these are likely sufficient to trigger all interesting code paths already.
411409
fee = provider.ConsumeIntegral<uint8_t>();
412-
size = provider.ConsumeIntegralInRange<uint32_t>(1, std::min<uint32_t>(0xff, max_tx_size));
410+
size = provider.ConsumeIntegralInRange<uint32_t>(1, 0xff);
413411
}
414412
FeePerWeight feerate{fee, size};
415413
// Create a real TxGraph::Ref.

src/txgraph.cpp

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ using ClusterSetIndex = uint32_t;
3535
/** Quality levels for cached cluster linearizations. */
3636
enum class QualityLevel
3737
{
38+
/** This is a singleton cluster consisting of a transaction that individually exceeds the
39+
* cluster size limit. It cannot be merged with anything. */
40+
OVERSIZED_SINGLETON,
3841
/** This cluster may have multiple disconnected components, which are all NEEDS_RELINEARIZE. */
3942
NEEDS_SPLIT,
4043
/** This cluster may have multiple disconnected components, which are all ACCEPTABLE. */
@@ -101,6 +104,10 @@ class Cluster
101104
{
102105
return m_quality == QualityLevel::OPTIMAL;
103106
}
107+
/** Whether this cluster is oversized. Note that no changes that can cause oversizedness are
108+
* ever applied, so the only way a materialized Cluster object can be oversized is by being
109+
* an individually oversized transaction singleton. */
110+
bool IsOversized() const noexcept { return m_quality == QualityLevel::OVERSIZED_SINGLETON; }
104111
/** Whether this cluster requires splitting. */
105112
bool NeedsSplitting() const noexcept
106113
{
@@ -225,7 +232,7 @@ class TxGraphImpl final : public TxGraph
225232
/** Which clusters are to be merged. GroupEntry::m_cluster_offset indexes into this. */
226233
std::vector<Cluster*> m_group_clusters;
227234
/** Whether at least one of the groups cannot be applied because it would result in a
228-
* Cluster that violates the cluster count limit. */
235+
* Cluster that violates the max cluster count or size limit. */
229236
bool m_group_oversized;
230237
};
231238

@@ -248,8 +255,11 @@ class TxGraphImpl final : public TxGraph
248255
/** Total number of transactions in this graph (sum of all transaction counts in all
249256
* Clusters, and for staging also those inherited from the main ClusterSet). */
250257
GraphIndex m_txcount{0};
258+
/** Total number of individually oversized transactions in the graph. */
259+
GraphIndex m_txcount_oversized{0};
251260
/** Whether this graph is oversized (if known). This roughly matches
252-
* m_group_data->m_group_oversized, but may be known even if m_group_data is not. */
261+
* m_group_data->m_group_oversized || (m_txcount_oversized > 0), but may be known even if
262+
* m_group_data is not. */
253263
std::optional<bool> m_oversized{false};
254264

255265
ClusterSet() noexcept = default;
@@ -446,8 +456,10 @@ class TxGraphImpl final : public TxGraph
446456
/** Get a reference to the ClusterSet at the specified level (which must exist). */
447457
ClusterSet& GetClusterSet(int level) noexcept;
448458
const ClusterSet& GetClusterSet(int level) const noexcept;
449-
/** Make a transaction not exist at a specified level. It must currently exist there. */
450-
void ClearLocator(int level, GraphIndex index) noexcept;
459+
/** Make a transaction not exist at a specified level. It must currently exist there.
460+
* oversized_tx indicates whether the transaction is an individually-oversized one
461+
* (OVERSIZED_SINGLETON). */
462+
void ClearLocator(int level, GraphIndex index, bool oversized_tx) noexcept;
451463
/** Find which Clusters in main conflict with ones in staging. */
452464
std::vector<Cluster*> GetConflicts() const noexcept;
453465
/** Clear an Entry's ChunkData. */
@@ -649,7 +661,7 @@ uint64_t Cluster::GetTotalTxSize() const noexcept
649661
return ret;
650662
}
651663

652-
void TxGraphImpl::ClearLocator(int level, GraphIndex idx) noexcept
664+
void TxGraphImpl::ClearLocator(int level, GraphIndex idx, bool oversized_tx) noexcept
653665
{
654666
auto& entry = m_entries[idx];
655667
auto& clusterset = GetClusterSet(level);
@@ -663,12 +675,14 @@ void TxGraphImpl::ClearLocator(int level, GraphIndex idx) noexcept
663675
}
664676
// Update the transaction count.
665677
--clusterset.m_txcount;
678+
clusterset.m_txcount_oversized -= oversized_tx;
666679
// If clearing main, adjust the status of Locators of this transaction in staging, if it exists.
667680
if (level == 0 && GetTopLevel() == 1) {
668681
if (entry.m_locator[1].IsRemoved()) {
669682
entry.m_locator[1].SetMissing();
670683
} else if (!entry.m_locator[1].IsPresent()) {
671684
--m_staging_clusterset->m_txcount;
685+
m_staging_clusterset->m_txcount_oversized -= oversized_tx;
672686
}
673687
}
674688
if (level == 0) ClearChunkData(entry);
@@ -799,7 +813,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span<GraphIndex>& to_remove
799813
entry.m_main_lin_index = LinearizationIndex(-1);
800814
}
801815
// - Mark it as missing/removed in the Entry's locator.
802-
graph.ClearLocator(m_level, idx);
816+
graph.ClearLocator(m_level, idx, m_quality == QualityLevel::OVERSIZED_SINGLETON);
803817
to_remove = to_remove.subspan(1);
804818
} while(!to_remove.empty());
805819

@@ -838,7 +852,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span<GraphIndex>& to_remove
838852
void Cluster::Clear(TxGraphImpl& graph) noexcept
839853
{
840854
for (auto i : m_linearization) {
841-
graph.ClearLocator(m_level, m_mapping[i]);
855+
graph.ClearLocator(m_level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON);
842856
}
843857
m_depgraph = {};
844858
m_linearization.clear();
@@ -1298,6 +1312,17 @@ void TxGraphImpl::GroupClusters(int level) noexcept
12981312
* to-be-merged group). */
12991313
std::vector<std::pair<std::pair<GraphIndex, GraphIndex>, uint64_t>> an_deps;
13001314

1315+
// Construct an an_clusters entry for every oversized cluster, including ones from levels below,
1316+
// as they may be inherited in this one.
1317+
for (int level_iter = 0; level_iter <= level; ++level_iter) {
1318+
for (auto& cluster : GetClusterSet(level_iter).m_clusters[int(QualityLevel::OVERSIZED_SINGLETON)]) {
1319+
auto graph_idx = cluster->GetClusterEntry(0);
1320+
auto cur_cluster = FindCluster(graph_idx, level);
1321+
if (cur_cluster == nullptr) continue;
1322+
an_clusters.emplace_back(cur_cluster, cur_cluster->m_sequence);
1323+
}
1324+
}
1325+
13011326
// Construct a an_clusters entry for every parent and child in the to-be-applied dependencies,
13021327
// and an an_deps entry for each dependency to be applied.
13031328
an_deps.reserve(clusterset.m_deps_to_add.size());
@@ -1314,7 +1339,7 @@ void TxGraphImpl::GroupClusters(int level) noexcept
13141339
an_deps.emplace_back(std::pair{par, chl}, chl_cluster->m_sequence);
13151340
}
13161341
// Sort and deduplicate an_clusters, so we end up with a sorted list of all involved Clusters
1317-
// to which dependencies apply.
1342+
// to which dependencies apply, or which are oversized.
13181343
std::sort(an_clusters.begin(), an_clusters.end(), [](auto& a, auto& b) noexcept { return a.second < b.second; });
13191344
an_clusters.erase(std::unique(an_clusters.begin(), an_clusters.end()), an_clusters.end());
13201345
// Sort an_deps by applying the same order to the involved child cluster.
@@ -1517,7 +1542,7 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept
15171542
// Nothing to do if there are no dependencies to be added.
15181543
if (clusterset.m_deps_to_add.empty()) return;
15191544
// Dependencies cannot be applied if it would result in oversized clusters.
1520-
if (clusterset.m_group_data->m_group_oversized) return;
1545+
if (clusterset.m_oversized == true) return;
15211546

15221547
// For each group of to-be-merged Clusters.
15231548
for (const auto& group_entry : clusterset.m_group_data->m_groups) {
@@ -1573,7 +1598,7 @@ void Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
15731598
void TxGraphImpl::MakeAcceptable(Cluster& cluster) noexcept
15741599
{
15751600
// Relinearize the Cluster if needed.
1576-
if (!cluster.NeedsSplitting() && !cluster.IsAcceptable()) {
1601+
if (!cluster.NeedsSplitting() && !cluster.IsAcceptable() && !cluster.IsOversized()) {
15771602
cluster.Relinearize(*this, 10000);
15781603
}
15791604
}
@@ -1603,7 +1628,7 @@ Cluster::Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feer
16031628
TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept
16041629
{
16051630
Assume(m_main_chunkindex_observers == 0 || GetTopLevel() != 0);
1606-
Assume(feerate.size > 0 && uint64_t(feerate.size) <= m_max_cluster_size);
1631+
Assume(feerate.size > 0);
16071632
// Construct a new Ref.
16081633
Ref ret;
16091634
// Construct a new Entry, and link it with the Ref.
@@ -1615,13 +1640,20 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept
16151640
GetRefGraph(ret) = this;
16161641
GetRefIndex(ret) = idx;
16171642
// Construct a new singleton Cluster (which is necessarily optimally linearized).
1643+
bool oversized = uint64_t(feerate.size) > m_max_cluster_size;
16181644
auto cluster = std::make_unique<Cluster>(m_next_sequence_counter++, *this, feerate, idx);
16191645
auto cluster_ptr = cluster.get();
16201646
int level = GetTopLevel();
16211647
auto& clusterset = GetClusterSet(level);
1622-
InsertCluster(level, std::move(cluster), QualityLevel::OPTIMAL);
1648+
InsertCluster(level, std::move(cluster), oversized ? QualityLevel::OVERSIZED_SINGLETON : QualityLevel::OPTIMAL);
16231649
cluster_ptr->Updated(*this);
16241650
++clusterset.m_txcount;
1651+
// Deal with individually oversized transactions.
1652+
if (oversized) {
1653+
++clusterset.m_txcount_oversized;
1654+
clusterset.m_oversized = true;
1655+
clusterset.m_group_data = std::nullopt;
1656+
}
16251657
// Return the Ref.
16261658
return ret;
16271659
}
@@ -1934,11 +1966,15 @@ bool TxGraphImpl::IsOversized(bool main_only) noexcept
19341966
// Return cached value if known.
19351967
return *clusterset.m_oversized;
19361968
}
1937-
// Find which Clusters will need to be merged together, as that is where the oversize
1938-
// property is assessed.
1939-
GroupClusters(level);
1940-
Assume(clusterset.m_group_data.has_value());
1941-
clusterset.m_oversized = clusterset.m_group_data->m_group_oversized;
1969+
ApplyRemovals(level);
1970+
if (clusterset.m_txcount_oversized > 0) {
1971+
clusterset.m_oversized = true;
1972+
} else {
1973+
// Find which Clusters will need to be merged together, as that is where the oversize
1974+
// property is assessed.
1975+
GroupClusters(level);
1976+
}
1977+
Assume(clusterset.m_oversized.has_value());
19421978
return *clusterset.m_oversized;
19431979
}
19441980

@@ -1958,6 +1994,7 @@ void TxGraphImpl::StartStaging() noexcept
19581994
// Copy statistics, precomputed data, and to-be-applied dependencies (only if oversized) to
19591995
// the new graph. To-be-applied removals will always be empty at this point.
19601996
m_staging_clusterset->m_txcount = m_main_clusterset.m_txcount;
1997+
m_staging_clusterset->m_txcount_oversized = m_main_clusterset.m_txcount_oversized;
19611998
m_staging_clusterset->m_deps_to_add = m_main_clusterset.m_deps_to_add;
19621999
m_staging_clusterset->m_group_data = m_main_clusterset.m_group_data;
19632000
m_staging_clusterset->m_oversized = m_main_clusterset.m_oversized;
@@ -1985,7 +2022,13 @@ void TxGraphImpl::AbortStaging() noexcept
19852022
if (!m_main_clusterset.m_group_data.has_value()) {
19862023
// In case m_oversized in main was kept after a Ref destruction while staging exists, we
19872024
// need to re-evaluate m_oversized now.
1988-
m_main_clusterset.m_oversized = std::nullopt;
2025+
if (m_main_clusterset.m_to_remove.empty() && m_main_clusterset.m_txcount_oversized > 0) {
2026+
// It is possible that a Ref destruction caused a removal in main while staging existed.
2027+
// In this case, m_txcount_oversized may be inaccurate.
2028+
m_main_clusterset.m_oversized = true;
2029+
} else {
2030+
m_main_clusterset.m_oversized = std::nullopt;
2031+
}
19892032
}
19902033
}
19912034

@@ -2019,6 +2062,7 @@ void TxGraphImpl::CommitStaging() noexcept
20192062
m_main_clusterset.m_group_data = std::move(m_staging_clusterset->m_group_data);
20202063
m_main_clusterset.m_oversized = std::move(m_staging_clusterset->m_oversized);
20212064
m_main_clusterset.m_txcount = std::move(m_staging_clusterset->m_txcount);
2065+
m_main_clusterset.m_txcount_oversized = std::move(m_staging_clusterset->m_txcount_oversized);
20222066
// Delete the old staging graph, after all its information was moved to main.
20232067
m_staging_clusterset.reset();
20242068
Compact();
@@ -2033,7 +2077,9 @@ void Cluster::SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcep
20332077
// Update the fee, remember that relinearization will be necessary, and update the Entries
20342078
// in the same Cluster.
20352079
m_depgraph.FeeRate(idx).fee = fee;
2036-
if (!NeedsSplitting()) {
2080+
if (m_quality == QualityLevel::OVERSIZED_SINGLETON) {
2081+
// Nothing to do.
2082+
} else if (!NeedsSplitting()) {
20372083
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::NEEDS_RELINEARIZE);
20382084
} else {
20392085
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::NEEDS_SPLIT);
@@ -2141,12 +2187,15 @@ void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const
21412187
assert(m_linearization.size() <= graph.m_max_cluster_count);
21422188
// The level must match the level the Cluster occurs in.
21432189
assert(m_level == level);
2144-
// The sum of their sizes cannot exceed m_max_cluster_size. Note that groups of to-be-merged
2145-
// clusters which would exceed this limit are marked oversized, which means they are never
2146-
// applied.
2147-
assert(GetTotalTxSize() <= graph.m_max_cluster_size);
2190+
// The sum of their sizes cannot exceed m_max_cluster_size, unless it is an individually
2191+
// oversized transaction singleton. Note that groups of to-be-merged clusters which would
2192+
// exceed this limit are marked oversized, which means they are never applied.
2193+
assert(m_quality == QualityLevel::OVERSIZED_SINGLETON || GetTotalTxSize() <= graph.m_max_cluster_size);
21482194
// m_quality and m_setindex are checked in TxGraphImpl::SanityCheck.
21492195

2196+
// OVERSIZED clusters are singletons.
2197+
assert(m_quality != QualityLevel::OVERSIZED_SINGLETON || m_linearization.size() == 1);
2198+
21502199
// Compute the chunking of m_linearization.
21512200
LinearizationChunking linchunking(m_depgraph, m_linearization);
21522201

@@ -2317,9 +2366,11 @@ void TxGraphImpl::SanityCheck() const
23172366
if (!clusterset.m_to_remove.empty()) compact_possible = false;
23182367
if (!clusterset.m_removed.empty()) compact_possible = false;
23192368

2320-
// If m_group_data exists, its m_group_oversized must match m_oversized.
2321-
if (clusterset.m_group_data.has_value()) {
2322-
assert(clusterset.m_oversized == clusterset.m_group_data->m_group_oversized);
2369+
// If m_group_data exists, and no outstanding removals remain, m_group_oversized must match
2370+
// m_group_oversized || (m_txcount_oversized > 0).
2371+
if (clusterset.m_group_data.has_value() && clusterset.m_to_remove.empty()) {
2372+
assert(clusterset.m_oversized ==
2373+
(clusterset.m_group_data->m_group_oversized || (clusterset.m_txcount_oversized > 0)));
23232374
}
23242375

23252376
// For non-top levels, m_oversized must be known (as it cannot change until the level

src/txgraph.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ class TxGraph
6464
virtual ~TxGraph() = default;
6565
/** Construct a new transaction with the specified feerate, and return a Ref to it.
6666
* If a staging graph exists, the new transaction is only created there. feerate.size must be
67-
* strictly positive, and cannot exceed the graph's max cluster size. In all further calls,
68-
* only Refs created by AddTransaction() are allowed to be passed to this TxGraph object (or
69-
* empty Ref objects). Ref objects may outlive the TxGraph they were created for. */
67+
* strictly positive. In all further calls, only Refs created by AddTransaction() are allowed
68+
* to be passed to this TxGraph object (or empty Ref objects). Ref objects may outlive the
69+
* TxGraph they were created for. */
7070
[[nodiscard]] virtual Ref AddTransaction(const FeePerWeight& feerate) noexcept = 0;
7171
/** Remove the specified transaction. If a staging graph exists, the removal only happens
7272
* there. This is a no-op if the transaction was already removed.

0 commit comments

Comments
 (0)