From 1d27b74c8e3bf055fb8b0a5fc5d664bd5048bec6 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 19 Mar 2025 16:22:25 -0400 Subject: [PATCH] txgraph: Add GetChunkFeerate function (feature) This adds a function to query the chunk feerate of a transaction, by caching it inside the Entry objects. --- src/test/fuzz/txgraph.cpp | 23 +++++++++++++++++++++- src/txgraph.cpp | 41 ++++++++++++++++++++++++++++++++++++++- src/txgraph.h | 9 +++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/test/fuzz/txgraph.cpp b/src/test/fuzz/txgraph.cpp index 367ded4b14..cec1cd56ed 100644 --- a/src/test/fuzz/txgraph.cpp +++ b/src/test/fuzz/txgraph.cpp @@ -321,6 +321,19 @@ FUZZ_TARGET(txgraph) assert(feerate == sim.graph.FeeRate(simpos)); } break; + } else if (command-- == 0) { + // GetChunkFeerate. + auto ref = pick_fn(); + auto feerate = real->GetChunkFeerate(*ref); + auto simpos = sim.Find(ref); + if (simpos == SimTxGraph::MISSING) { + assert(feerate.IsEmpty()); + } else { + // Just do some quick checks that the reported value is in range. A full + // recomputation of expected chunk feerates is done at the end. + assert(feerate.size >= sim.graph.FeeRate(simpos).size); + } + break; } else if (command-- == 0) { // GetAncestors/GetDescendants. auto ref = pick_fn(); @@ -405,13 +418,21 @@ FUZZ_TARGET(txgraph) simlin.push_back(simpos); } // Construct a chunking object for the simulated graph, using the reported cluster - // linearization as ordering. + // linearization as ordering, and compare it against the reported chunk feerates. cluster_linearize::LinearizationChunking simlinchunk(sim.graph, simlin); + DepGraphIndex idx{0}; for (unsigned chunknum = 0; chunknum < simlinchunk.NumChunksLeft(); ++chunknum) { auto chunk = simlinchunk.GetChunk(chunknum); // Require that the chunks of cluster linearizations are connected (this must // be the case as all linearizations inside are PostLinearized). assert(sim.graph.IsConnected(chunk.transactions)); + // Check the chunk feerates of all transactions in the cluster. + while (chunk.transactions.Any()) { + assert(chunk.transactions[simlin[idx]]); + chunk.transactions.Reset(simlin[idx]); + assert(chunk.feerate == real->GetChunkFeerate(*cluster[idx])); + ++idx; + } } } } diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 378a2947c8..4b49cd6c6f 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -221,6 +221,8 @@ private: Ref* m_ref{nullptr}; /** Which Cluster and position therein this Entry appears in. */ Locator m_locator; + /** The chunk feerate of this transaction (if not missing). */ + FeePerWeight m_chunk_feerate; }; /** The set of all transactions. GraphIndex values index into this. */ @@ -301,6 +303,7 @@ public: void SetTransactionFee(const Ref&, int64_t fee) noexcept final; bool Exists(const Ref& arg) noexcept final; + FeePerWeight GetChunkFeerate(const Ref& arg) noexcept final; FeePerWeight GetIndividualFeerate(const Ref& arg) noexcept final; std::vector GetCluster(const Ref& arg) noexcept final; std::vector GetAncestors(const Ref& arg) noexcept final; @@ -317,6 +320,24 @@ void Cluster::Updated(TxGraphImpl& graph) noexcept auto& entry = graph.m_entries[m_mapping[idx]]; entry.m_locator.SetPresent(this, idx); } + + // Compute its chunking and store its information in the Entry's m_chunk_feerate. + LinearizationChunking chunking(m_depgraph, m_linearization); + LinearizationIndex lin_idx{0}; + // Iterate over the chunks. + for (unsigned chunk_idx = 0; chunk_idx < chunking.NumChunksLeft(); ++chunk_idx) { + auto chunk = chunking.GetChunk(chunk_idx); + Assume(chunk.transactions.Any()); + // Iterate over the transactions in the linearization, which must match those in chunk. + do { + DepGraphIndex idx = m_linearization[lin_idx++]; + GraphIndex graph_idx = m_mapping[idx]; + auto& entry = graph.m_entries[graph_idx]; + entry.m_chunk_feerate = FeePerWeight::FromFeeFrac(chunk.feerate); + Assume(chunk.transactions[idx]); + chunk.transactions.Reset(idx); + } while(chunk.transactions.Any()); + } } void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span& to_remove) noexcept @@ -1108,6 +1129,23 @@ FeePerWeight TxGraphImpl::GetIndividualFeerate(const Ref& arg) noexcept return cluster->GetIndividualFeerate(m_entries[GetRefIndex(arg)].m_locator.index); } +FeePerWeight TxGraphImpl::GetChunkFeerate(const Ref& arg) noexcept +{ + // Return the empty FeePerWeight if the passed Ref is empty. + if (GetRefGraph(arg) == nullptr) return {}; + Assume(GetRefGraph(arg) == this); + // Apply all removals and dependencies, as the result might be inaccurate otherwise. + ApplyDependencies(); + // Find the cluster the argument is in, and return the empty FeePerWeight if it isn't in any. + auto cluster = m_entries[GetRefIndex(arg)].m_locator.cluster; + if (cluster == nullptr) return {}; + // Make sure the Cluster has an acceptable quality level, and then return the transaction's + // chunk feerate. + MakeAcceptable(*cluster); + const auto& entry = m_entries[GetRefIndex(arg)]; + return entry.m_chunk_feerate; +} + void Cluster::SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcept { // Make sure the specified DepGraphIndex exists in this Cluster. @@ -1159,10 +1197,11 @@ void Cluster::SanityCheck(const TxGraphImpl& graph) const // Check that the Entry has a locator pointing back to this Cluster & position within it. assert(entry.m_locator.cluster == this); assert(entry.m_locator.index == lin_pos); - // Check linearization position. + // Check linearization position and chunk feerate. if (!linchunking.GetChunk(0).transactions[lin_pos]) { linchunking.MarkDone(linchunking.GetChunk(0).transactions); } + assert(entry.m_chunk_feerate == linchunking.GetChunk(0).feerate); // If this Cluster has an acceptable quality level, its chunks must be connected. if (IsAcceptable()) { assert(m_depgraph.IsConnected(linchunking.GetChunk(0).transactions)); diff --git a/src/txgraph.h b/src/txgraph.h index 709b4ba7c2..f92cfbd795 100644 --- a/src/txgraph.h +++ b/src/txgraph.h @@ -29,6 +29,12 @@ static constexpr unsigned CLUSTER_COUNT_LIMIT{64}; * * For more explanation, see https://delvingbitcoin.org/t/introduction-to-cluster-linearization/1032 * + * This linearization is partitioned into chunks: groups of transactions that according to this + * order would be mined together. Each chunk consists of the highest-feerate prefix of what remains + * of the linearization after removing previous chunks. TxGraph guarantees that the maintained + * linearization always results in chunks consisting of transactions that are connected. A chunk's + * transactions always belong to the same cluster. + * * The interface is designed to accommodate an implementation that only stores the transitive * closure of dependencies, so if B spends C, it does not distinguish between "A spending B" and * "A spending both B and C". @@ -82,6 +88,9 @@ public: /** Get the individual transaction feerate of transaction arg. Returns the empty FeePerWeight * if arg does not exist. */ virtual FeePerWeight GetIndividualFeerate(const Ref& arg) noexcept = 0; + /** Get the feerate of the chunk which transaction arg is in. Returns the empty FeePerWeight if + * arg does not exist. */ + virtual FeePerWeight GetChunkFeerate(const Ref& arg) noexcept = 0; /** Get pointers to all transactions in the cluster which arg is in. The transactions will be * returned in graph order. Returns {} if arg does not exist in the graph. */ virtual std::vector GetCluster(const Ref& arg) noexcept = 0;