txgraph: Add GetChunkFeerate function (feature)

This adds a function to query the chunk feerate of a transaction, by caching it
inside the Entry objects.
This commit is contained in:
Pieter Wuille 2025-03-19 16:22:25 -04:00
parent c80aecc24d
commit 1d27b74c8e
3 changed files with 71 additions and 2 deletions

View file

@ -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;
}
}
}
}

View file

@ -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<Ref*> GetCluster(const Ref& arg) noexcept final;
std::vector<Ref*> 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<GraphIndex>& 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));

View file

@ -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<Ref*> GetCluster(const Ref& arg) noexcept = 0;