Skip to content

Commit 394dbe2

Browse files
committed
txgraph: Introduce BlockBuilder interface (feature)
This interface lets one iterate efficiently over the chunks of the main graph in a TxGraph, in the same order as CompareMainOrder. Each chunk can be marked as "included" or "skipped" (and in the latter case, dependent chunks will be skipped).
1 parent 883df36 commit 394dbe2

File tree

3 files changed

+254
-8
lines changed

3 files changed

+254
-8
lines changed

src/test/fuzz/txgraph.cpp

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,24 @@ FUZZ_TARGET(txgraph)
269269
sims.reserve(2);
270270
sims.emplace_back(max_count);
271271

272+
/** Struct encapsulating information about a BlockBuilder that's currently live. */
273+
struct BlockBuilderData
274+
{
275+
/** BlockBuilder object from real. */
276+
std::unique_ptr<TxGraph::BlockBuilder> builder;
277+
/** The set of transactions marked as included in *builder. */
278+
SimTxGraph::SetType included;
279+
/** The set of transactions marked as included or skipped in *builder. */
280+
SimTxGraph::SetType done;
281+
/** The last chunk feerate returned by *builder. IsEmpty() if none yet. */
282+
FeePerWeight last_feerate;
283+
284+
BlockBuilderData(std::unique_ptr<TxGraph::BlockBuilder> builder_in) : builder(std::move(builder_in)) {}
285+
};
286+
287+
/** Currently active block builders. */
288+
std::vector<BlockBuilderData> block_builders;
289+
272290
/** Function to pick any Ref (for either sim in sims: from sim.simmap or sim.removed, or the
273291
* empty Ref). */
274292
auto pick_fn = [&]() noexcept -> TxGraph::Ref* {
@@ -342,13 +360,20 @@ FUZZ_TARGET(txgraph)
342360
LIMITED_WHILE(provider.remaining_bytes() > 0, 200) {
343361
// Read a one-byte command.
344362
int command = provider.ConsumeIntegral<uint8_t>();
363+
int orig_command = command;
364+
345365
// Treat the lowest bit of a command as a flag (which selects a variant of some of the
346366
// operations), and the second-lowest bit as a way of selecting main vs. staging, and leave
347367
// the rest of the bits in command.
348368
bool alt = command & 1;
349369
bool use_main = command & 2;
350370
command >>= 2;
351371

372+
/** Use the bottom 2 bits of command to select an entry in the block_builders vector (if
373+
* any). These use the same bits as alt/use_main, so don't use those in actions below
374+
* where builder_idx is used as well. */
375+
int builder_idx = block_builders.empty() ? -1 : int((orig_command & 3) % block_builders.size());
376+
352377
// Provide convenient aliases for the top simulated graph (main, or staging if it exists),
353378
// one for the simulated graph selected based on use_main (for operations that can operate
354379
// on both graphs), and one that always refers to the main graph.
@@ -359,7 +384,7 @@ FUZZ_TARGET(txgraph)
359384
// Keep decrementing command for each applicable operation, until one is hit. Multiple
360385
// iterations may be necessary.
361386
while (true) {
362-
if (top_sim.GetTransactionCount() < SimTxGraph::MAX_TRANSACTIONS && command-- == 0) {
387+
if ((block_builders.empty() || sims.size() > 1) && top_sim.GetTransactionCount() < SimTxGraph::MAX_TRANSACTIONS && command-- == 0) {
363388
// AddTransaction.
364389
int64_t fee;
365390
int32_t size;
@@ -381,7 +406,7 @@ FUZZ_TARGET(txgraph)
381406
// Move it in place.
382407
*ref_loc = std::move(ref);
383408
break;
384-
} else if (top_sim.GetTransactionCount() + top_sim.removed.size() > 1 && command-- == 0) {
409+
} else if ((block_builders.empty() || sims.size() > 1) && top_sim.GetTransactionCount() + top_sim.removed.size() > 1 && command-- == 0) {
385410
// AddDependency.
386411
auto par = pick_fn();
387412
auto chl = pick_fn();
@@ -395,7 +420,7 @@ FUZZ_TARGET(txgraph)
395420
top_sim.AddDependency(par, chl);
396421
real->AddDependency(*par, *chl);
397422
break;
398-
} else if (top_sim.removed.size() < 100 && command-- == 0) {
423+
} else if ((block_builders.empty() || sims.size() > 1) && top_sim.removed.size() < 100 && command-- == 0) {
399424
// RemoveTransaction. Either all its ancestors or all its descendants are also
400425
// removed (if any), to make sure TxGraph's reordering of removals and dependencies
401426
// has no effect.
@@ -425,7 +450,7 @@ FUZZ_TARGET(txgraph)
425450
}
426451
sel_sim.removed.pop_back();
427452
break;
428-
} else if (command-- == 0) {
453+
} else if (block_builders.empty() && command-- == 0) {
429454
// ~Ref (of any transaction).
430455
std::vector<TxGraph::Ref*> to_destroy;
431456
to_destroy.push_back(pick_fn());
@@ -447,7 +472,7 @@ FUZZ_TARGET(txgraph)
447472
}
448473
}
449474
break;
450-
} else if (command-- == 0) {
475+
} else if (block_builders.empty() && command-- == 0) {
451476
// SetTransactionFee.
452477
int64_t fee;
453478
if (alt) {
@@ -578,7 +603,7 @@ FUZZ_TARGET(txgraph)
578603
sims.back().modified = SimTxGraph::SetType{};
579604
real->StartStaging();
580605
break;
581-
} else if (sims.size() > 1 && command-- == 0) {
606+
} else if (block_builders.empty() && sims.size() > 1 && command-- == 0) {
582607
// CommitStaging.
583608
real->CommitStaging();
584609
sims.erase(sims.begin());
@@ -664,6 +689,65 @@ FUZZ_TARGET(txgraph)
664689
assert(FeeRateCompare(real_staged_diagram[i], real_staged_diagram[i - 1]) <= 0);
665690
}
666691
break;
692+
} else if (block_builders.size() < 4 && !main_sim.IsOversized() && command-- == 0) {
693+
// GetBlockBuilder.
694+
block_builders.emplace_back(real->GetBlockBuilder());
695+
break;
696+
} else if (!block_builders.empty() && command-- == 0) {
697+
// ~BlockBuilder.
698+
block_builders.erase(block_builders.begin() + builder_idx);
699+
break;
700+
} else if (!block_builders.empty() && command-- == 0) {
701+
// BlockBuilder::GetCurrentChunk, followed by Include/Skip.
702+
auto& builder_data = block_builders[builder_idx];
703+
auto new_included = builder_data.included;
704+
auto new_done = builder_data.done;
705+
auto chunk = builder_data.builder->GetCurrentChunk();
706+
if (chunk) {
707+
// Chunk feerates must be monotonously decreasing.
708+
if (!builder_data.last_feerate.IsEmpty()) {
709+
assert(!(chunk->second >> builder_data.last_feerate));
710+
}
711+
builder_data.last_feerate = chunk->second;
712+
// Verify the contents of GetCurrentChunk.
713+
FeePerWeight sum_feerate;
714+
for (TxGraph::Ref* ref : chunk->first) {
715+
// Each transaction in the chunk must exist in the main graph.
716+
auto simpos = main_sim.Find(ref);
717+
assert(simpos != SimTxGraph::MISSING);
718+
// Verify the claimed chunk feerate.
719+
sum_feerate += main_sim.graph.FeeRate(simpos);
720+
// Make sure no transaction is reported twice.
721+
assert(!new_done[simpos]);
722+
new_done.Set(simpos);
723+
// The concatenation of all included transactions must be topologically valid.
724+
new_included.Set(simpos);
725+
assert(main_sim.graph.Ancestors(simpos).IsSubsetOf(new_included));
726+
}
727+
assert(sum_feerate == chunk->second);
728+
} else {
729+
// When we reach the end, if nothing was skipped, the entire graph should have
730+
// been reported.
731+
if (builder_data.done == builder_data.included) {
732+
assert(builder_data.done.Count() == main_sim.GetTransactionCount());
733+
}
734+
}
735+
// Possibly invoke GetCurrentChunk() again, which should give the same result.
736+
if ((orig_command % 7) >= 5) {
737+
auto chunk2 = builder_data.builder->GetCurrentChunk();
738+
assert(chunk == chunk2);
739+
}
740+
// Skip or include.
741+
if ((orig_command % 5) >= 3) {
742+
// Skip.
743+
builder_data.builder->Skip();
744+
} else {
745+
// Include.
746+
builder_data.builder->Include();
747+
builder_data.included = new_included;
748+
}
749+
builder_data.done = new_done;
750+
break;
667751
}
668752
}
669753
}
@@ -718,6 +802,28 @@ FUZZ_TARGET(txgraph)
718802
}
719803
}
720804

805+
// The same order should be obtained through a BlockBuilder as implied by CompareMainOrder,
806+
// if nothing is skipped.
807+
auto builder = real->GetBlockBuilder();
808+
std::vector<SimTxGraph::Pos> vec_builder;
809+
while (auto chunk = builder->GetCurrentChunk()) {
810+
FeePerWeight sum;
811+
for (TxGraph::Ref* ref : chunk->first) {
812+
// The reported chunk feerate must match the chunk feerate obtained by asking
813+
// it for each of the chunk's transactions individually.
814+
assert(real->GetMainChunkFeerate(*ref) == chunk->second);
815+
// Verify the chunk feerate matches the sum of the reported individual feerates.
816+
sum += real->GetIndividualFeerate(*ref);
817+
// Chunks must contain transactions that exist in the graph.
818+
auto simpos = sims[0].Find(ref);
819+
assert(simpos != SimTxGraph::MISSING);
820+
vec_builder.push_back(simpos);
821+
}
822+
assert(sum == chunk->second);
823+
builder->Include();
824+
}
825+
assert(vec_builder == vec1);
826+
721827
// Check that the implied ordering gives rise to a combined diagram that matches the
722828
// diagram constructed from the individual cluster linearization chunkings.
723829
auto main_real_diagram = get_diagram_fn(/*main_only=*/true);
@@ -848,6 +954,8 @@ FUZZ_TARGET(txgraph)
848954
// Sanity check again (because invoking inspectors may modify internal unobservable state).
849955
real->SanityCheck();
850956

957+
// Kill the block builders.
958+
block_builders.clear();
851959
// Kill the TxGraph object.
852960
real.reset();
853961
// Kill the simulated graphs, with all remaining Refs in it. If any, this verifies that Refs

src/txgraph.cpp

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class Cluster
191191
class TxGraphImpl final : public TxGraph
192192
{
193193
friend class Cluster;
194+
friend class BlockBuilderImpl;
194195
private:
195196
/** Internal RNG. */
196197
FastRandomContext m_rng;
@@ -319,7 +320,7 @@ class TxGraphImpl final : public TxGraph
319320
/** Index of ChunkData objects, indexing the last transaction in each chunk in the main
320321
* graph. */
321322
ChunkIndex m_main_chunkindex;
322-
/** Number of index-observing objects in existence. */
323+
/** Number of index-observing objects in existence (BlockBuilderImpls). */
323324
size_t m_main_chunkindex_observers{0};
324325

325326
/** A Locator that describes whether, where, and in which Cluster an Entry appears.
@@ -543,6 +544,8 @@ class TxGraphImpl final : public TxGraph
543544
GraphIndex CountDistinctClusters(std::span<const Ref* const> refs, bool main_only = false) noexcept final;
544545
std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>> GetMainStagingDiagrams() noexcept final;
545546

547+
std::unique_ptr<BlockBuilder> GetBlockBuilder() noexcept final;
548+
546549
void SanityCheck() const final;
547550
};
548551

@@ -562,6 +565,34 @@ const TxGraphImpl::ClusterSet& TxGraphImpl::GetClusterSet(int level) const noexc
562565
return *m_staging_clusterset;
563566
}
564567

568+
/** Implementation of the TxGraph::BlockBuilder interface. */
569+
class BlockBuilderImpl final : public TxGraph::BlockBuilder
570+
{
571+
/** Which TxGraphImpl this object is doing block building for. It will have its
572+
* m_main_chunkindex_observers incremented as long as this BlockBuilderImpl exists. */
573+
TxGraphImpl* const m_graph;
574+
/** Clusters which we're not including further transactions from. */
575+
std::set<Cluster*> m_excluded_clusters;
576+
/** Iterator to the current chunk in the chunk index. end() if nothing further remains. */
577+
TxGraphImpl::ChunkIndex::const_iterator m_cur_iter;
578+
/** Which cluster the current chunk belongs to, so we can exclude further transactions from it
579+
* when that chunk is skipped. */
580+
Cluster* m_cur_cluster;
581+
582+
// Move m_cur_iter / m_cur_cluster to the next acceptable chunk.
583+
void Next() noexcept;
584+
585+
public:
586+
/** Construct a new BlockBuilderImpl to build blocks for the provided graph. */
587+
BlockBuilderImpl(TxGraphImpl& graph) noexcept;
588+
589+
// Implement the public interface.
590+
~BlockBuilderImpl() final;
591+
std::optional<std::pair<std::vector<TxGraph::Ref*>, FeePerWeight>> GetCurrentChunk() noexcept final;
592+
void Include() noexcept final;
593+
void Skip() noexcept final;
594+
};
595+
565596
void TxGraphImpl::ClearLocator(int level, GraphIndex idx) noexcept
566597
{
567598
auto& entry = m_entries[idx];
@@ -2266,6 +2297,88 @@ void TxGraphImpl::DoWork() noexcept
22662297
}
22672298
}
22682299

2300+
void BlockBuilderImpl::Next() noexcept
2301+
{
2302+
// Don't do anything if we're already done.
2303+
if (m_cur_iter == m_graph->m_main_chunkindex.end()) return;
2304+
while (true) {
2305+
// Advance the pointer, and stop if we reach the end.
2306+
++m_cur_iter;
2307+
m_cur_cluster = nullptr;
2308+
if (m_cur_iter == m_graph->m_main_chunkindex.end()) break;
2309+
// Find the cluster pointed to by m_cur_iter.
2310+
const auto& chunk_data = *m_cur_iter;
2311+
const auto& chunk_end_entry = m_graph->m_entries[chunk_data.m_graph_index];
2312+
m_cur_cluster = chunk_end_entry.m_locator[0].cluster;
2313+
// If we previously skipped a chunk from this cluster we cannot include more from it.
2314+
if (!m_excluded_clusters.contains(m_cur_cluster)) break;
2315+
}
2316+
}
2317+
2318+
std::optional<std::pair<std::vector<TxGraph::Ref*>, FeePerWeight>> BlockBuilderImpl::GetCurrentChunk() noexcept
2319+
{
2320+
std::optional<std::pair<std::vector<TxGraph::Ref*>, FeePerWeight>> ret;
2321+
// Populate the return value if we are not done.
2322+
if (m_cur_iter != m_graph->m_main_chunkindex.end()) {
2323+
ret.emplace();
2324+
const auto& chunk_data = *m_cur_iter;
2325+
const auto& chunk_end_entry = m_graph->m_entries[chunk_data.m_graph_index];
2326+
ret->first.resize(chunk_data.m_chunk_count);
2327+
auto start_pos = chunk_end_entry.m_main_lin_index + 1 - chunk_data.m_chunk_count;
2328+
Assume(m_cur_cluster);
2329+
m_cur_cluster->GetClusterRefs(*m_graph, ret->first, start_pos);
2330+
ret->second = chunk_end_entry.m_main_chunk_feerate;
2331+
}
2332+
return ret;
2333+
}
2334+
2335+
BlockBuilderImpl::BlockBuilderImpl(TxGraphImpl& graph) noexcept : m_graph(&graph)
2336+
{
2337+
// Make sure all clusters in main are up to date, and acceptable.
2338+
m_graph->MakeAllAcceptable(0);
2339+
// There cannot remain any inapplicable dependencies (only possible if main is oversized).
2340+
Assume(m_graph->m_main_clusterset.m_deps_to_add.empty());
2341+
// Remember that this object is observing the graph's index, so that we can detect concurrent
2342+
// modifications.
2343+
++m_graph->m_main_chunkindex_observers;
2344+
// Find the first chunk.
2345+
m_cur_iter = m_graph->m_main_chunkindex.begin();
2346+
m_cur_cluster = nullptr;
2347+
if (m_cur_iter != m_graph->m_main_chunkindex.end()) {
2348+
// Find the cluster pointed to by m_cur_iter.
2349+
const auto& chunk_data = *m_cur_iter;
2350+
const auto& chunk_end_entry = m_graph->m_entries[chunk_data.m_graph_index];
2351+
m_cur_cluster = chunk_end_entry.m_locator[0].cluster;
2352+
}
2353+
}
2354+
2355+
BlockBuilderImpl::~BlockBuilderImpl()
2356+
{
2357+
Assume(m_graph->m_main_chunkindex_observers > 0);
2358+
// Permit modifications to the main graph again after destroying the BlockBuilderImpl.
2359+
--m_graph->m_main_chunkindex_observers;
2360+
}
2361+
2362+
void BlockBuilderImpl::Include() noexcept
2363+
{
2364+
// The actual inclusion of the chunk is done by the calling code. All we have to do is switch
2365+
// to the next chunk.
2366+
Next();
2367+
}
2368+
2369+
void BlockBuilderImpl::Skip() noexcept
2370+
{
2371+
// When skipping a chunk we need to not include anything more of the cluster, as that could make
2372+
// the result topologically invalid.
2373+
if (m_cur_cluster != nullptr) m_excluded_clusters.insert(m_cur_cluster);
2374+
Next();
2375+
}
2376+
2377+
std::unique_ptr<TxGraph::BlockBuilder> TxGraphImpl::GetBlockBuilder() noexcept
2378+
{
2379+
return std::make_unique<BlockBuilderImpl>(*this);
2380+
}
2381+
22692382
} // namespace
22702383

22712384
TxGraph::Ref::~Ref()

src/txgraph.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#include <compare>
6-
#include <stdint.h>
76
#include <memory>
7+
#include <optional>
8+
#include <stdint.h>
89
#include <vector>
10+
#include <utility>
911

1012
#include <util/feefrac.h>
1113

@@ -168,6 +170,29 @@ class TxGraph
168170
* usable without type-conversion. */
169171
virtual std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>> GetMainStagingDiagrams() noexcept = 0;
170172

173+
/** Interface returned by GetBlockBuilder. */
174+
class BlockBuilder
175+
{
176+
protected:
177+
/** Make constructor non-public (use TxGraph::GetBlockBuilder()). */
178+
BlockBuilder() noexcept = default;
179+
public:
180+
/** Support safe inheritance. */
181+
virtual ~BlockBuilder() = default;
182+
/** Get the chunk that is currently suggested to be included, plus its feerate, if any. */
183+
virtual std::optional<std::pair<std::vector<Ref*>, FeePerWeight>> GetCurrentChunk() noexcept = 0;
184+
/** Mark the current chunk as included, and progress to the next one. */
185+
virtual void Include() noexcept = 0;
186+
/** Mark the current chunk as skipped, and progress to the next one. Further chunks from
187+
* the same cluster as the current one will not be reported anymore. */
188+
virtual void Skip() noexcept = 0;
189+
};
190+
191+
/** Construct a block builder, drawing chunks in order, from the main graph, which cannot be
192+
* oversized. While the returned object exists, no mutators on the main graph are allowed.
193+
* The BlockBuilder object must not outlive the TxGraph it was created with. */
194+
virtual std::unique_ptr<BlockBuilder> GetBlockBuilder() noexcept = 0;
195+
171196
/** Perform an internal consistency check on this object. */
172197
virtual void SanityCheck() const = 0;
173198

0 commit comments

Comments
 (0)