Skip to content

Commit 1c24c62

Browse files
committed
clusterlin: merge two DepGraph fuzz tests into simulation test
This combines the clusterlin_add_dependency and clusterlin_cluster_serialization fuzz tests into a single clusterlin_depgraph_sim fuzz test. This tests starts from an empty DepGraph and performs a arbitrary number of AddTransaction, AddDependencies, and RemoveTransactions operations on it, and compares the resulting state with a naive reimplementation.
1 parent 0606e66 commit 1c24c62

File tree

1 file changed

+139
-85
lines changed

1 file changed

+139
-85
lines changed

src/test/fuzz/cluster_linearize.cpp

Lines changed: 139 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -241,103 +241,157 @@ std::vector<ClusterIndex> ReadLinearization(const DepGraph<BS>& depgraph, SpanRe
241241

242242
} // namespace
243243

244-
FUZZ_TARGET(clusterlin_add_dependencies)
244+
FUZZ_TARGET(clusterlin_depgraph_sim)
245245
{
246-
// Verify that computing a DepGraph from a cluster, or building it step by step using
247-
// AddDependencies has the same effect.
246+
// Simulation test to verify the full behavior of DepGraph.
248247

249248
FuzzedDataProvider provider(buffer.data(), buffer.size());
250-
auto rng_seed = provider.ConsumeIntegral<uint64_t>();
251-
InsecureRandomContext rng(rng_seed);
252-
253-
// Construct a cluster of a certain length, with no dependencies.
254-
auto num_tx = provider.ConsumeIntegralInRange<ClusterIndex>(2, TestBitSet::Size());
255-
Cluster<TestBitSet> cluster(num_tx, std::pair{FeeFrac{0, 1}, TestBitSet{}});
256-
// Construct the corresponding DepGraph object (also no dependencies).
257-
DepGraph depgraph_batch(cluster);
258-
SanityCheck(depgraph_batch);
259-
// Read (parents, child) pairs, and add the dependencies to the cluster and depgraph.
260-
std::vector<std::pair<ClusterIndex, ClusterIndex>> deps_list;
261-
LIMITED_WHILE(provider.remaining_bytes() > 0, TestBitSet::Size()) {
262-
const auto parents_mask = provider.ConsumeIntegralInRange<uint64_t>(0, (uint64_t{1} << num_tx) - 1);
263-
auto child = provider.ConsumeIntegralInRange<ClusterIndex>(0, num_tx - 1);
264-
265-
auto parents_mask_shifted = parents_mask;
266-
TestBitSet deps;
267-
for (ClusterIndex i = 0; i < num_tx; ++i) {
268-
if (parents_mask_shifted & 1) {
269-
deps.Set(i);
270-
cluster[child].second.Set(i);
249+
250+
/** Real DepGraph being tested. */
251+
DepGraph<TestBitSet> real;
252+
/** Simulated DepGraph (sim[i] is std::nullopt if position i does not exist; otherwise,
253+
* sim[i]->first is its individual feerate, and sim[i]->second is its set of ancestors. */
254+
std::array<std::optional<std::pair<FeeFrac, TestBitSet>>, TestBitSet::Size()> sim;
255+
/** The number of non-nullopt position in sim. */
256+
ClusterIndex num_tx_sim{0};
257+
258+
/** Read a valid index of a transaction from the provider. */
259+
auto idx_fn = [&]() {
260+
auto offset = provider.ConsumeIntegralInRange<ClusterIndex>(0, num_tx_sim - 1);
261+
for (ClusterIndex i = 0; i < sim.size(); ++i) {
262+
if (!sim[i].has_value()) continue;
263+
if (offset == 0) return i;
264+
--offset;
265+
}
266+
assert(false);
267+
return ClusterIndex(-1);
268+
};
269+
270+
/** Read a valid subset of the transactions from the provider. */
271+
auto subset_fn = [&]() {
272+
auto range = (uint64_t{1} << num_tx_sim) - 1;
273+
const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range);
274+
auto mask_shifted = mask;
275+
TestBitSet subset;
276+
for (ClusterIndex i = 0; i < sim.size(); ++i) {
277+
if (!sim[i].has_value()) continue;
278+
if (mask_shifted & 1) {
279+
subset.Set(i);
271280
}
272-
parents_mask_shifted >>= 1;
281+
mask_shifted >>= 1;
273282
}
274-
assert(parents_mask_shifted == 0);
275-
depgraph_batch.AddDependencies(deps, child);
276-
for (auto i : deps) {
277-
deps_list.emplace_back(i, child);
278-
assert(depgraph_batch.Ancestors(child)[i]);
279-
assert(depgraph_batch.Descendants(i)[child]);
283+
assert(mask_shifted == 0);
284+
return subset;
285+
};
286+
287+
/** Read any set of transactions from the provider (including unused positions). */
288+
auto set_fn = [&]() {
289+
auto range = (uint64_t{1} << sim.size()) - 1;
290+
const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range);
291+
TestBitSet set;
292+
for (ClusterIndex i = 0; i < sim.size(); ++i) {
293+
if ((mask >> i) & 1) {
294+
set.Set(i);
295+
}
280296
}
281-
}
282-
// Sanity check the result.
283-
SanityCheck(depgraph_batch);
284-
// Verify that the resulting DepGraph matches one recomputed from the cluster.
285-
assert(DepGraph(cluster) == depgraph_batch);
286-
287-
DepGraph<TestBitSet> depgraph_individual;
288-
// Add all transactions to depgraph_individual.
289-
for (const auto& [feerate, parents] : cluster) {
290-
depgraph_individual.AddTransaction(feerate);
291-
}
292-
SanityCheck(depgraph_individual);
293-
// Add all individual dependencies to depgraph_individual in randomized order.
294-
std::shuffle(deps_list.begin(), deps_list.end(), rng);
295-
for (auto [parent, child] : deps_list) {
296-
depgraph_individual.AddDependencies(TestBitSet::Singleton(parent), child);
297-
assert(depgraph_individual.Ancestors(child)[parent]);
298-
assert(depgraph_individual.Descendants(parent)[child]);
299-
}
300-
// Sanity check and compare again the batched version.
301-
SanityCheck(depgraph_individual);
302-
assert(depgraph_individual == depgraph_batch);
303-
}
297+
return set;
298+
};
304299

305-
FUZZ_TARGET(clusterlin_cluster_serialization)
306-
{
307-
// Verify that any graph of transactions has its ancestry correctly computed by DepGraph, and
308-
// if it is a DAG, that it can be serialized as a DepGraph in a way that roundtrips. This
309-
// guarantees that any acyclic cluster has a corresponding DepGraph serialization.
300+
/** Propagate ancestor information in sim. */
301+
auto anc_update_fn = [&]() {
302+
while (true) {
303+
bool updates{false};
304+
for (ClusterIndex chl = 0; chl < sim.size(); ++chl) {
305+
if (!sim[chl].has_value()) continue;
306+
for (auto par : sim[chl]->second) {
307+
if (!sim[chl]->second.IsSupersetOf(sim[par]->second)) {
308+
sim[chl]->second |= sim[par]->second;
309+
updates = true;
310+
}
311+
}
312+
}
313+
if (!updates) break;
314+
}
315+
};
310316

311-
FuzzedDataProvider provider(buffer.data(), buffer.size());
317+
/** Compare the state of transaction i in the simulation with the real one. */
318+
auto check_fn = [&](ClusterIndex i) {
319+
// Compare used positions.
320+
assert(real.Positions()[i] == sim[i].has_value());
321+
if (sim[i].has_value()) {
322+
// Compare feerate.
323+
assert(real.FeeRate(i) == sim[i]->first);
324+
// Compare ancestors (note that SanityCheck verifies correspondence between ancestors
325+
// and descendants, so we can restrict ourselves to ancestors here).
326+
assert(real.Ancestors(i) == sim[i]->second);
327+
}
328+
};
312329

313-
// Construct a cluster in a naive way (using a FuzzedDataProvider-based serialization).
314-
Cluster<TestBitSet> cluster;
315-
auto num_tx = provider.ConsumeIntegralInRange<ClusterIndex>(1, 32);
316-
cluster.resize(num_tx);
317-
for (ClusterIndex i = 0; i < num_tx; ++i) {
318-
cluster[i].first.size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
319-
cluster[i].first.fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
320-
for (ClusterIndex j = 0; j < num_tx; ++j) {
321-
if (i == j) continue;
322-
if (provider.ConsumeBool()) cluster[i].second.Set(j);
330+
LIMITED_WHILE(provider.remaining_bytes() > 0, 1000) {
331+
uint8_t command = provider.ConsumeIntegral<uint8_t>();
332+
if (num_tx_sim == 0 || ((command % 3) <= 0 && num_tx_sim < TestBitSet::Size())) {
333+
// AddTransaction.
334+
auto fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
335+
auto size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
336+
FeeFrac feerate{fee, size};
337+
// Apply to DepGraph.
338+
auto idx = real.AddTransaction(feerate);
339+
// Verify that the returned index is correct.
340+
assert(!sim[idx].has_value());
341+
for (ClusterIndex i = 0; i < TestBitSet::Size(); ++i) {
342+
if (!sim[i].has_value()) {
343+
assert(idx == i);
344+
break;
345+
}
346+
}
347+
// Update sim.
348+
sim[idx] = {feerate, TestBitSet::Singleton(idx)};
349+
++num_tx_sim;
350+
continue;
351+
}
352+
if ((command % 3) <= 1 && num_tx_sim > 0) {
353+
// AddDependencies.
354+
ClusterIndex child = idx_fn();
355+
auto parents = subset_fn();
356+
// Apply to DepGraph.
357+
real.AddDependencies(parents, child);
358+
// Apply to sim.
359+
sim[child]->second |= parents;
360+
continue;
361+
}
362+
if (num_tx_sim > 0) {
363+
// Remove transactions.
364+
auto del = set_fn();
365+
// Propagate all ancestry information before deleting anything in the simulation (as
366+
// intermediary transactions may be deleted which impact connectivity).
367+
anc_update_fn();
368+
// Compare the state of the transactions being deleted.
369+
for (auto i : del) check_fn(i);
370+
// Apply to DepGraph.
371+
real.RemoveTransactions(del);
372+
// Apply to sim.
373+
for (ClusterIndex i = 0; i < sim.size(); ++i) {
374+
if (sim[i].has_value()) {
375+
if (del[i]) {
376+
--num_tx_sim;
377+
sim[i] = std::nullopt;
378+
} else {
379+
sim[i]->second -= del;
380+
}
381+
}
382+
}
383+
continue;
323384
}
385+
// This should be unreachable (one of the 3 above actions should always be possible).
386+
assert(false);
324387
}
325388

326-
// Construct dependency graph, and verify it matches the cluster (which includes a round-trip
327-
// check for the serialization).
328-
DepGraph depgraph(cluster);
329-
VerifyDepGraphFromCluster(cluster, depgraph);
330-
331-
// Remove an arbitrary subset (in order to construct a graph with holes) and verify that it
332-
// still sanity checks (incl. round-tripping serialization).
333-
uint64_t del = provider.ConsumeIntegralInRange<uint64_t>(1, (uint64_t{1} << TestBitSet::Size()) - 1);
334-
TestBitSet setdel;
335-
for (ClusterIndex i = 0; i < TestBitSet::Size(); ++i) {
336-
if (del & 1) setdel.Set(i);
337-
del >>= 1;
338-
}
339-
depgraph.RemoveTransactions(setdel);
340-
SanityCheck(depgraph);
389+
// Compare the real obtained depgraph against the simulation.
390+
anc_update_fn();
391+
for (ClusterIndex i = 0; i < sim.size(); ++i) check_fn(i);
392+
assert(real.TxCount() == num_tx_sim);
393+
// Sanity check the result (which includes round-tripping serialization, if applicable).
394+
SanityCheck(real);
341395
}
342396

343397
FUZZ_TARGET(clusterlin_depgraph_serialization)

0 commit comments

Comments
 (0)