Skip to content

Commit ee57e93

Browse files
committed
txgraph: Add internal sanity check function (tests)
To make testing more powerful, expose a function to perform an internal sanity check on the state of a TxGraph. This is especially important as TxGraphImpl contains many redundantly represented pieces of information: * graph contains clusters, which refer to entries, but the entries refer back * graph maintains pointers to Ref objects, which point back to the graph. This lets us make sure they are always in sync.
1 parent 05abf33 commit ee57e93

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

src/test/fuzz/txgraph.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ FUZZ_TARGET(txgraph)
363363
}
364364
}
365365
}
366+
367+
// After running all modifications, perform an internal sanity check (before invoking
368+
// inspectors that may modify the internal state).
369+
real->SanityCheck();
370+
366371
// Compare simple properties of the graph with the simulation.
367372
assert(real->GetTransactionCount() == sim.GetTransactionCount());
368373

@@ -411,6 +416,9 @@ FUZZ_TARGET(txgraph)
411416
}
412417
}
413418

419+
// Sanity check again (because invoking inspectors may modify internal unobservable state).
420+
real->SanityCheck();
421+
414422
// Remove all remaining transactions, because Refs cannot be destroyed otherwise (this will be
415423
// addressed in a follow-up commit).
416424
for (auto i : sim.graph.Positions()) {

src/txgraph.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include <compare>
1414
#include <memory>
15+
#include <set>
1516
#include <span>
1617
#include <utility>
1718

@@ -94,6 +95,8 @@ class Cluster
9495
}
9596
/** Get the number of transactions in this Cluster. */
9697
LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); }
98+
/** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */
99+
GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; }
97100
/** Only called by Graph::SwapIndexes. */
98101
void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept { m_mapping[cluster_idx] = graph_idx; }
99102
/** Push changes to Cluster and its linearization to the TxGraphImpl Entry objects. */
@@ -126,6 +129,10 @@ class Cluster
126129
FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept;
127130
/** Modify the fee of a Cluster element. */
128131
void SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcept;
132+
133+
// Debugging functions.
134+
135+
void SanityCheck(const TxGraphImpl& graph) const;
129136
};
130137

131138
/** The transaction graph.
@@ -189,6 +196,8 @@ class TxGraphImpl final : public TxGraph
189196
void SetMissing() noexcept { cluster = nullptr; index = 0; }
190197
/** Mark this Locator as present, in the specified Cluster. */
191198
void SetPresent(Cluster* c, DepGraphIndex i) noexcept { cluster = c; index = i; }
199+
/** Check if this Locator is missing. */
200+
bool IsMissing() const noexcept { return cluster == nullptr && index == 0; }
192201
/** Check if this Locator is present (in some Cluster). */
193202
bool IsPresent() const noexcept { return cluster != nullptr; }
194203
};
@@ -285,6 +294,8 @@ class TxGraphImpl final : public TxGraph
285294
std::vector<Ref*> GetAncestors(const Ref& arg) noexcept final;
286295
std::vector<Ref*> GetDescendants(const Ref& arg) noexcept final;
287296
GraphIndex GetTransactionCount() noexcept final;
297+
298+
void SanityCheck() const final;
288299
};
289300

290301
void Cluster::Updated(TxGraphImpl& graph) noexcept
@@ -1098,6 +1109,118 @@ void TxGraphImpl::SetTransactionFee(const Ref& ref, int64_t fee) noexcept
10981109
}
10991110
}
11001111

1112+
void Cluster::SanityCheck(const TxGraphImpl& graph) const
1113+
{
1114+
// There must be an m_mapping for each m_depgraph position (including holes).
1115+
assert(m_depgraph.PositionRange() == m_mapping.size());
1116+
// The linearization for this Cluster must contain every transaction once.
1117+
assert(m_depgraph.TxCount() == m_linearization.size());
1118+
// m_quality and m_setindex are checked in TxGraphImpl::SanityCheck.
1119+
1120+
// Compute the chunking of m_linearization.
1121+
LinearizationChunking linchunking(m_depgraph, m_linearization);
1122+
1123+
// Verify m_linearization.
1124+
SetType m_done;
1125+
assert(m_depgraph.IsAcyclic());
1126+
for (auto lin_pos : m_linearization) {
1127+
assert(lin_pos < m_mapping.size());
1128+
const auto& entry = graph.m_entries[m_mapping[lin_pos]];
1129+
// Check that the linearization is topological.
1130+
m_done.Set(lin_pos);
1131+
assert(m_done.IsSupersetOf(m_depgraph.Ancestors(lin_pos)));
1132+
// Check that the Entry has a locator pointing back to this Cluster & position within it.
1133+
assert(entry.m_locator.cluster == this);
1134+
assert(entry.m_locator.index == lin_pos);
1135+
// Check linearization position.
1136+
if (!linchunking.GetChunk(0).transactions[lin_pos]) {
1137+
linchunking.MarkDone(linchunking.GetChunk(0).transactions);
1138+
}
1139+
// If this Cluster has an acceptable quality level, its chunks must be connected.
1140+
if (IsAcceptable()) {
1141+
assert(m_depgraph.IsConnected(linchunking.GetChunk(0).transactions));
1142+
}
1143+
}
1144+
// Verify that each element of m_depgraph occured in m_linearization.
1145+
assert(m_done == m_depgraph.Positions());
1146+
}
1147+
1148+
void TxGraphImpl::SanityCheck() const
1149+
{
1150+
/** Which GraphIndexes ought to occur in m_unlinked, based on m_entries. */
1151+
std::set<GraphIndex> expected_unlinked;
1152+
/** Which Clusters ought to occur in m_clusters, based on m_entries. */
1153+
std::set<const Cluster*> expected_clusters;
1154+
1155+
// Go over all Entry objects in m_entries.
1156+
for (GraphIndex idx = 0; idx < m_entries.size(); ++idx) {
1157+
const auto& entry = m_entries[idx];
1158+
if (entry.m_ref == nullptr) {
1159+
// Unlinked Entry must have indexes appear in m_unlinked.
1160+
expected_unlinked.insert(idx);
1161+
} else {
1162+
// Every non-unlinked Entry must have a Ref that points back to it.
1163+
assert(GetRefGraph(*entry.m_ref) == this);
1164+
assert(GetRefIndex(*entry.m_ref) == idx);
1165+
}
1166+
const auto& locator = entry.m_locator;
1167+
// Every Locator must be in exactly one of these 2 states.
1168+
assert(locator.IsMissing() + locator.IsPresent() == 1);
1169+
if (locator.IsPresent()) {
1170+
// Verify that the Cluster agrees with where the Locator claims the transaction is.
1171+
assert(locator.cluster->GetClusterEntry(locator.index) == idx);
1172+
// Remember that we expect said Cluster to appear in the m_clusters.
1173+
expected_clusters.insert(locator.cluster);
1174+
}
1175+
1176+
}
1177+
1178+
std::set<const Cluster*> actual_clusters;
1179+
// For all quality levels...
1180+
for (int qual = 0; qual < int(QualityLevel::NONE); ++qual) {
1181+
QualityLevel quality{qual};
1182+
const auto& quality_clusters = m_clusters[qual];
1183+
// ... for all clusters in them ...
1184+
for (ClusterSetIndex setindex = 0; setindex < quality_clusters.size(); ++setindex) {
1185+
const auto& cluster = *quality_clusters[setindex];
1186+
// Remember we saw this Cluster (only if it is non-empty; empty Clusters aren't
1187+
// expected to be referenced by the Entry vector).
1188+
if (cluster.GetTxCount() != 0) {
1189+
actual_clusters.insert(&cluster);
1190+
}
1191+
// Sanity check the cluster, according to the Cluster's internal rules.
1192+
cluster.SanityCheck(*this);
1193+
// Check that the cluster's quality and setindex matches its position in the quality list.
1194+
assert(cluster.m_quality == quality);
1195+
assert(cluster.m_setindex == setindex);
1196+
}
1197+
}
1198+
1199+
// Verify that all to-be-removed transactions have valid identifiers, and aren't removed yet.
1200+
for (GraphIndex idx : m_to_remove) {
1201+
assert(idx < m_entries.size());
1202+
assert(m_entries[idx].m_locator.IsPresent());
1203+
}
1204+
1205+
// Verify that all to-be-added dependencies have valid identifiers.
1206+
for (auto [par_idx, chl_idx] : m_deps_to_add) {
1207+
assert(par_idx != chl_idx);
1208+
assert(par_idx < m_entries.size());
1209+
assert(chl_idx < m_entries.size());
1210+
}
1211+
1212+
// Verify that the actually encountered clusters match the ones occurring in Entry vector.
1213+
assert(actual_clusters == expected_clusters);
1214+
1215+
// Verify that the contents of m_unlinked matches what was expected based on the Entry vector.
1216+
std::set<GraphIndex> actual_unlinked(m_unlinked.begin(), m_unlinked.end());
1217+
assert(actual_unlinked == expected_unlinked);
1218+
1219+
// If no to-be-removed transactions, or to-be-added dependencies remain, m_unlinked must be
1220+
// empty (to prevent memory leaks due to an ever-growing m_entries vector).
1221+
if (m_to_remove.empty() && m_deps_to_add.empty()) assert(actual_unlinked.empty());
1222+
}
1223+
11011224
} // namespace
11021225

11031226
TxGraph::Ref::~Ref()

src/txgraph.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ class TxGraph
9494
/** Get the total number of transactions in the graph. */
9595
virtual GraphIndex GetTransactionCount() noexcept = 0;
9696

97+
/** Perform an internal consistency check on this object. */
98+
virtual void SanityCheck() const = 0;
99+
97100
protected:
98101
// Allow TxGraph::Ref to call UpdateRef and UnlinkRef.
99102
friend class TxGraph::Ref;

0 commit comments

Comments
 (0)