Skip to content

Commit c72c8d5

Browse files
committed
txgraph: compare sequence numbers instead of Cluster* (bugfix)
This makes fuzz testing more deterministic, by avoiding the (arbitrary) pointer value ordering in comparing transactions.
1 parent b2bb27f commit c72c8d5

File tree

2 files changed

+61
-26
lines changed

2 files changed

+61
-26
lines changed

src/test/fuzz/txgraph.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,14 @@ struct SimTxGraph
206206
ret.push_back(ptr);
207207
}
208208
}
209-
// Deduplicate.
210-
std::sort(ret.begin(), ret.end());
211-
ret.erase(std::unique(ret.begin(), ret.end()), ret.end());
212-
// Replace input.
213-
arg = std::move(ret);
209+
// Construct deduplicated version in input (do not use std::sort/std::unique for
210+
// deduplication as it'd rely on non-deterministic pointer comparison).
211+
arg.clear();
212+
for (auto ptr : ret) {
213+
if (std::find(arg.begin(), arg.end(), ptr) == arg.end()) {
214+
arg.push_back(ptr);
215+
}
216+
}
214217
}
215218
};
216219

src/txgraph.cpp

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,16 @@ class Cluster
7070
ClusterSetIndex m_setindex{ClusterSetIndex(-1)};
7171
/** Which level this Cluster is at in the graph (-1=not inserted, 0=main, 1=staging). */
7272
int m_level{-1};
73+
/** Sequence number for this Cluster (for tie-breaking comparison between equal-chunk-feerate
74+
transactions in distinct clusters). */
75+
uint64_t m_sequence;
7376

7477
public:
78+
Cluster() noexcept = delete;
7579
/** Construct an empty Cluster. */
76-
Cluster() noexcept = default;
80+
explicit Cluster(uint64_t sequence) noexcept;
7781
/** Construct a singleton Cluster. */
78-
explicit Cluster(TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept;
82+
explicit Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept;
7983

8084
// Cannot move or copy (would invalidate Cluster* in Locator and ClusterSet). */
8185
Cluster(const Cluster&) = delete;
@@ -157,6 +161,7 @@ class Cluster
157161
void SanityCheck(const TxGraphImpl& graph, int level) const;
158162
};
159163

164+
160165
/** The transaction graph, including staged changes.
161166
*
162167
* The overall design of the data structure consists of 3 interlinked representations:
@@ -244,6 +249,8 @@ class TxGraphImpl final : public TxGraph
244249
ClusterSet m_main_clusterset;
245250
/** The staging ClusterSet, if any. */
246251
std::optional<ClusterSet> m_staging_clusterset;
252+
/** Next sequence number to assign to created Clusters. */
253+
uint64_t m_next_sequence_counter{0};
247254

248255
/** A Locator that describes whether, where, and in which Cluster an Entry appears.
249256
* Every Entry has MAX_LEVELS locators, as it may appear in one Cluster per level.
@@ -315,6 +322,18 @@ class TxGraphImpl final : public TxGraph
315322
/** Set of Entries which have no linked Ref anymore. */
316323
std::vector<GraphIndex> m_unlinked;
317324

325+
/** Compare two Cluster* by their m_sequence value (while supporting nullptr). */
326+
static std::strong_ordering CompareClusters(Cluster* a, Cluster* b) noexcept
327+
{
328+
// The nullptr pointer compares before everything else.
329+
if (a == nullptr || b == nullptr) {
330+
return (a != nullptr) <=> (b != nullptr);
331+
}
332+
// If neither pointer is nullptr, compare the Clusters' sequence numbers.
333+
Assume(a == b || a->m_sequence != b->m_sequence);
334+
return a->m_sequence <=> b->m_sequence;
335+
}
336+
318337
public:
319338
/** Construct a new TxGraphImpl with the specified maximum cluster count. */
320339
explicit TxGraphImpl(DepGraphIndex max_cluster_count) noexcept :
@@ -569,15 +588,15 @@ std::vector<Cluster*> TxGraphImpl::GetConflicts() const noexcept
569588
}
570589
}
571590
// Deduplicate the result (the same Cluster may appear multiple times).
572-
std::sort(ret.begin(), ret.end());
591+
std::sort(ret.begin(), ret.end(), [](Cluster* a, Cluster* b) noexcept { return CompareClusters(a, b) < 0; });
573592
ret.erase(std::unique(ret.begin(), ret.end()), ret.end());
574593
return ret;
575594
}
576595

577596
Cluster* Cluster::CopyToStaging(TxGraphImpl& graph) const noexcept
578597
{
579598
// Construct an empty Cluster.
580-
auto ret = std::make_unique<Cluster>();
599+
auto ret = std::make_unique<Cluster>(graph.m_next_sequence_counter++);
581600
auto ptr = ret.get();
582601
// Copy depgraph, mapping, and linearization/
583602
ptr->m_depgraph = m_depgraph;
@@ -710,7 +729,7 @@ bool Cluster::Split(TxGraphImpl& graph) noexcept
710729
}
711730
first = false;
712731
// Construct a new Cluster to hold the found component.
713-
auto new_cluster = std::make_unique<Cluster>();
732+
auto new_cluster = std::make_unique<Cluster>(graph.m_next_sequence_counter++);
714733
new_clusters.push_back(new_cluster.get());
715734
// Remember that all the component's transactions go to this new Cluster. The positions
716735
// will be determined below, so use -1 for now.
@@ -956,9 +975,11 @@ void TxGraphImpl::ApplyRemovals(int up_to_level) noexcept
956975
if (cluster != nullptr) PullIn(cluster);
957976
}
958977
}
959-
// Group the set of to-be-removed entries by Cluster*.
978+
// Group the set of to-be-removed entries by Cluster::m_sequence.
960979
std::sort(to_remove.begin(), to_remove.end(), [&](GraphIndex a, GraphIndex b) noexcept {
961-
return std::less{}(m_entries[a].m_locator[level].cluster, m_entries[b].m_locator[level].cluster);
980+
Cluster* cluster_a = m_entries[a].m_locator[level].cluster;
981+
Cluster* cluster_b = m_entries[b].m_locator[level].cluster;
982+
return CompareClusters(cluster_a, cluster_b) < 0;
962983
});
963984
// Process per Cluster.
964985
std::span to_remove_span{to_remove};
@@ -1103,16 +1124,16 @@ void TxGraphImpl::GroupClusters(int level) noexcept
11031124
}
11041125
// Sort and deduplicate an_clusters, so we end up with a sorted list of all involved Clusters
11051126
// to which dependencies apply.
1106-
std::sort(an_clusters.begin(), an_clusters.end());
1127+
std::sort(an_clusters.begin(), an_clusters.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.first, b.first) < 0; });
11071128
an_clusters.erase(std::unique(an_clusters.begin(), an_clusters.end()), an_clusters.end());
11081129

1109-
// Sort the dependencies by child Cluster.
1130+
// Sort the dependencies by child Cluster::m_sequence.
11101131
std::sort(clusterset.m_deps_to_add.begin(), clusterset.m_deps_to_add.end(), [&](auto& a, auto& b) noexcept {
11111132
auto [_a_par, a_chl] = a;
11121133
auto [_b_par, b_chl] = b;
11131134
auto a_chl_cluster = FindCluster(a_chl, level);
11141135
auto b_chl_cluster = FindCluster(b_chl, level);
1115-
return std::less{}(a_chl_cluster, b_chl_cluster);
1136+
return CompareClusters(a_chl_cluster, b_chl_cluster) < 0;
11161137
});
11171138

11181139
// Run the union-find algorithm to to find partitions of the input Clusters which need to be
@@ -1132,13 +1153,13 @@ void TxGraphImpl::GroupClusters(int level) noexcept
11321153
* tree for this partition. */
11331154
unsigned rank;
11341155
};
1135-
/** Information about each input Cluster. Sorted by Cluster* pointer. */
1156+
/** Information about each input Cluster. Sorted by Cluster::m_sequence. */
11361157
std::vector<PartitionData> partition_data;
11371158

11381159
/** Given a Cluster, find its corresponding PartitionData. */
11391160
auto locate_fn = [&](Cluster* arg) noexcept -> PartitionData* {
11401161
auto it = std::lower_bound(partition_data.begin(), partition_data.end(), arg,
1141-
[](auto& a, Cluster* ptr) noexcept { return a.cluster < ptr; });
1162+
[](auto& a, Cluster* ptr) noexcept { return CompareClusters(a.cluster, ptr) < 0; });
11421163
Assume(it != partition_data.end());
11431164
Assume(it->cluster == arg);
11441165
return &*it;
@@ -1219,7 +1240,7 @@ void TxGraphImpl::GroupClusters(int level) noexcept
12191240
while (deps_it != clusterset.m_deps_to_add.end()) {
12201241
auto [par, chl] = *deps_it;
12211242
auto chl_cluster = FindCluster(chl, level);
1222-
if (std::greater{}(chl_cluster, data.cluster)) break;
1243+
if (CompareClusters(chl_cluster, data.cluster) > 0) break;
12231244
// Skip dependencies that apply to earlier Clusters (those necessary are for
12241245
// deleted transactions, as otherwise we'd have processed them already).
12251246
if (chl_cluster == data.cluster) {
@@ -1234,8 +1255,8 @@ void TxGraphImpl::GroupClusters(int level) noexcept
12341255

12351256
// Sort both an_clusters and an_deps by representative of the partition they are in, grouping
12361257
// all those applying to the same partition together.
1237-
std::sort(an_deps.begin(), an_deps.end(), [](auto& a, auto& b) noexcept { return a.second < b.second; });
1238-
std::sort(an_clusters.begin(), an_clusters.end(), [](auto& a, auto& b) noexcept { return a.second < b.second; });
1258+
std::sort(an_deps.begin(), an_deps.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.second, b.second) < 0; });
1259+
std::sort(an_clusters.begin(), an_clusters.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.second, b.second) < 0; });
12391260

12401261
// Translate the resulting cluster groups to the m_group_data structure, and the dependencies
12411262
// back to m_deps_to_add.
@@ -1390,7 +1411,10 @@ void TxGraphImpl::MakeAllAcceptable(int level) noexcept
13901411
}
13911412
}
13921413

1393-
Cluster::Cluster(TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept
1414+
Cluster::Cluster(uint64_t sequence) noexcept : m_sequence{sequence} {}
1415+
1416+
Cluster::Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept :
1417+
m_sequence{sequence}
13941418
{
13951419
// Create a new transaction in the DepGraph, and remember its position in m_mapping.
13961420
auto cluster_idx = m_depgraph.AddTransaction(feerate);
@@ -1410,7 +1434,7 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept
14101434
GetRefGraph(ret) = this;
14111435
GetRefIndex(ret) = idx;
14121436
// Construct a new singleton Cluster (which is necessarily optimally linearized).
1413-
auto cluster = std::make_unique<Cluster>(*this, feerate, idx);
1437+
auto cluster = std::make_unique<Cluster>(m_next_sequence_counter++, *this, feerate, idx);
14141438
auto cluster_ptr = cluster.get();
14151439
int level = GetTopLevel();
14161440
auto& clusterset = GetClusterSet(level);
@@ -1606,7 +1630,7 @@ std::vector<TxGraph::Ref*> TxGraphImpl::GetAncestorsUnion(std::span<const Ref* c
16061630
matches.emplace_back(cluster, m_entries[GetRefIndex(*arg)].m_locator[cluster->m_level].index);
16071631
}
16081632
// Group by Cluster.
1609-
std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return std::less{}(a.first, b.first); });
1633+
std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.first, b.first) < 0; });
16101634
// Dispatch to the Clusters.
16111635
std::span match_span(matches);
16121636
std::vector<TxGraph::Ref*> ret;
@@ -1639,7 +1663,7 @@ std::vector<TxGraph::Ref*> TxGraphImpl::GetDescendantsUnion(std::span<const Ref*
16391663
matches.emplace_back(cluster, m_entries[GetRefIndex(*arg)].m_locator[cluster->m_level].index);
16401664
}
16411665
// Group by Cluster.
1642-
std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return std::less{}(a.first, b.first); });
1666+
std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.first, b.first) < 0; });
16431667
// Dispatch to the Clusters.
16441668
std::span match_span(matches);
16451669
std::vector<TxGraph::Ref*> ret;
@@ -1867,7 +1891,9 @@ std::strong_ordering TxGraphImpl::CompareMainOrder(const Ref& a, const Ref& b) n
18671891
if (feerate_cmp < 0) return std::strong_ordering::less;
18681892
if (feerate_cmp > 0) return std::strong_ordering::greater;
18691893
// Compare Cluster* as tie-break for equal chunk feerates.
1870-
if (locator_a.cluster != locator_b.cluster) return locator_a.cluster <=> locator_b.cluster;
1894+
if (locator_a.cluster != locator_b.cluster) {
1895+
return CompareClusters(locator_a.cluster, locator_b.cluster);
1896+
}
18711897
// As final tie-break, compare position within cluster linearization.
18721898
return entry_a.m_main_lin_index <=> entry_b.m_main_lin_index;
18731899
}
@@ -1889,7 +1915,7 @@ TxGraph::GraphIndex TxGraphImpl::CountDistinctClusters(std::span<const Ref* cons
18891915
if (cluster != nullptr) clusters.push_back(cluster);
18901916
}
18911917
// Count the number of distinct elements in clusters.
1892-
std::sort(clusters.begin(), clusters.end());
1918+
std::sort(clusters.begin(), clusters.end(), [](Cluster* a, Cluster* b) noexcept { return CompareClusters(a, b) < 0; });
18931919
Cluster* last{nullptr};
18941920
GraphIndex ret{0};
18951921
for (Cluster* cluster : clusters) {
@@ -1951,6 +1977,8 @@ void TxGraphImpl::SanityCheck() const
19511977
std::set<const Cluster*> expected_clusters[MAX_LEVELS];
19521978
/** Which GraphIndexes ought to occur in ClusterSet::m_removed, based on m_entries. */
19531979
std::set<GraphIndex> expected_removed[MAX_LEVELS];
1980+
/** Which Cluster::m_sequence values have been encountered. */
1981+
std::set<uint64_t> sequences;
19541982
/** Whether compaction is possible in the current state. */
19551983
bool compact_possible{true};
19561984

@@ -2004,6 +2032,10 @@ void TxGraphImpl::SanityCheck() const
20042032
// ... for all clusters in them ...
20052033
for (ClusterSetIndex setindex = 0; setindex < quality_clusters.size(); ++setindex) {
20062034
const auto& cluster = *quality_clusters[setindex];
2035+
// Check the sequence number.
2036+
assert(cluster.m_sequence < m_next_sequence_counter);
2037+
assert(sequences.count(cluster.m_sequence) == 0);
2038+
sequences.insert(cluster.m_sequence);
20072039
// Remember we saw this Cluster (only if it is non-empty; empty Clusters aren't
20082040
// expected to be referenced by the Entry vector).
20092041
if (cluster.GetTxCount() != 0) {

0 commit comments

Comments
 (0)