mempool/txgraph: remove unneeded functionality

This commit is contained in:
Olaoluwa Osuntokun 2025-10-01 15:10:02 -07:00
parent a049e38501
commit af3e8d2352
6 changed files with 0 additions and 551 deletions

View file

@ -46,20 +46,6 @@ type Config struct {
// mempool eviction policies in the caller.
MaxNodes int
// MaxEdges limits the total number of parent-child relationships. This
// provides defense against attacks that try to create extremely
// connected transaction graphs to degrade performance.
MaxEdges int
// EnableCaching enables memoization of expensive computations like
// ancestor/descendant counts. This trades memory for speed, which is
// beneficial in production but may complicate debugging.
EnableCaching bool
// CacheTimeout defines how long cached computation results remain
// valid. Shorter timeouts trade freshness for computation cost.
CacheTimeout time.Duration
// MaxPackageSize limits the number of transactions in a package.
// Bitcoin Core uses 101 (25 ancestors + 25 descendants + 1 root),
// enforced here to prevent package relay DoS attacks.
@ -75,9 +61,6 @@ type Config struct {
func DefaultConfig() *Config {
return &Config{
MaxNodes: 100000,
MaxEdges: 200000,
EnableCaching: true,
CacheTimeout: 5 * time.Second,
MaxPackageSize: 101,
}
}
@ -444,61 +427,6 @@ func (g *TxGraph) HasTransaction(hash chainhash.Hash) bool {
return exists
}
// AddEdge adds an edge between two nodes.
func (g *TxGraph) AddEdge(parentHash, childHash chainhash.Hash) error {
g.mu.Lock()
defer g.mu.Unlock()
parent, parentExists := g.nodes[parentHash]
child, childExists := g.nodes[childHash]
if !parentExists || !childExists {
return ErrNodeNotFound
}
// Check if edge already exists.
if _, exists := parent.Children[childHash]; exists {
return nil // Already connected
}
// Check for cycles.
if g.wouldCreateCycle(parent, child) {
return ErrCycleDetected
}
// Add edge.
parent.Children[childHash] = child
child.Parents[parentHash] = parent
atomic.AddInt32(&g.metrics.edgeCount, 1)
// Update clusters.
g.mergeNodeClusters(parent, child)
return nil
}
// RemoveEdge removes an edge between two nodes.
func (g *TxGraph) RemoveEdge(parentHash, childHash chainhash.Hash) error {
g.mu.Lock()
defer g.mu.Unlock()
parent, parentExists := g.nodes[parentHash]
child, childExists := g.nodes[childHash]
if !parentExists || !childExists {
return ErrNodeNotFound
}
// Remove edge if it exists.
if _, exists := parent.Children[childHash]; exists {
delete(parent.Children, childHash)
delete(child.Parents, parentHash)
atomic.AddInt32(&g.metrics.edgeCount, -1)
}
return nil
}
// GetAncestors returns all ancestors of a transaction up to maxDepth.
func (g *TxGraph) GetAncestors(hash chainhash.Hash,
maxDepth int) map[chainhash.Hash]*TxGraphNode {
@ -511,13 +439,6 @@ func (g *TxGraph) GetAncestors(hash chainhash.Hash,
return nil
}
// Check cache if enabled.
if g.config.EnableCaching &&
time.Since(node.cachedMetrics.LastUpdated) < g.config.CacheTimeout {
// For now, skip cache and compute directly.
// TODO: Implement proper caching.
}
ancestors := make(map[chainhash.Hash]*TxGraphNode)
visited := make(map[chainhash.Hash]bool)
g.collectAncestorsRecursive(node, ancestors, visited, 0, maxDepth)
@ -798,8 +719,6 @@ func (g *TxGraph) createNewCluster(node *TxGraphNode) {
}
cluster.Nodes[node.TxHash] = node
cluster.Roots = []*TxGraphNode{node}
cluster.Leaves = []*TxGraphNode{node}
g.indexes.clusters[clusterID] = cluster
g.indexes.nodeToCluster[node.TxHash] = clusterID
@ -822,9 +741,6 @@ func (g *TxGraph) addToCluster(node *TxGraphNode, clusterID ClusterID) {
g.indexes.nodeToCluster[node.TxHash] = clusterID
node.Metadata.ClusterID = clusterID
// Update roots and leaves.
g.updateClusterBoundaries(cluster)
}
// mergeClusters merges multiple clusters into one.
@ -878,7 +794,6 @@ func (g *TxGraph) mergeClusters(
}
targetCluster.Size = len(targetCluster.Nodes)
g.updateClusterBoundaries(targetCluster)
}
// mergeNodeClusters merges clusters when adding an edge.
@ -897,18 +812,3 @@ func (g *TxGraph) mergeNodeClusters(parent, child *TxGraphNode) {
g.mergeClusters(parent, clusters)
}
// updateClusterBoundaries updates roots and leaves of a cluster.
func (g *TxGraph) updateClusterBoundaries(cluster *TxCluster) {
cluster.Roots = nil
cluster.Leaves = nil
for _, node := range cluster.Nodes {
if len(node.Parents) == 0 {
cluster.Roots = append(cluster.Roots, node)
}
if len(node.Children) == 0 {
cluster.Leaves = append(cluster.Leaves, node)
}
}
}

View file

@ -191,16 +191,6 @@ func TestGraphEdges(t *testing.T) {
metrics := g.GetMetrics()
require.Equal(t, 2, metrics.NodeCount)
require.Equal(t, 1, metrics.EdgeCount)
err = g.RemoveEdge(*parent.Hash(), *child.Hash())
require.NoError(t, err)
// Edge removal should update both nodes' relationship maps and
// decrement the edge count metric.
parentNode, _ = g.GetNode(*parent.Hash())
childNode, _ = g.GetNode(*child.Hash())
require.Len(t, parentNode.Children, 0)
require.Len(t, childNode.Parents, 0)
}
// TestGraphAncestorsDescendants verifies that ancestor and descendant
@ -256,30 +246,6 @@ func TestGraphAncestorsDescendants(t *testing.T) {
require.NotNil(t, descendants[*txs[2].Hash()])
}
// TestCycleDetection verifies that the graph prevents cycles, which would
// violate the DAG property required for transaction dependencies. Cycles
// would make ancestor/descendant queries infinite loop and break topological
// ordering for block template construction.
func TestCycleDetection(t *testing.T) {
g := New(DefaultConfig())
tx1, desc1 := createTestTx(nil, 1)
tx2, desc2 := createTestTx(
[]wire.OutPoint{{Hash: *tx1.Hash(), Index: 0}}, 1,
)
err := g.AddTransaction(tx1, desc1)
require.NoError(t, err)
err = g.AddTransaction(tx2, desc2)
require.NoError(t, err)
// Attempting to add an edge that would create a cycle (tx2 -> tx1
// when tx1 -> tx2 already exists) must be rejected to maintain the
// DAG invariant.
err = g.AddEdge(*tx2.Hash(), *tx1.Hash())
require.ErrorIs(t, err, ErrCycleDetected)
}
// TestClusterManagement verifies that transactions are correctly grouped
// into clusters (connected components) and that clusters merge when a
// transaction bridges two previously separate clusters. This is essential
@ -393,46 +359,6 @@ func TestGetNodeCount(t *testing.T) {
require.Equal(t, 2, g.GetNodeCount())
}
// TestAddEdgeErrors tests error cases in AddEdge.
func TestAddEdgeErrors(t *testing.T) {
g := New(DefaultConfig())
// Try to add edge between non-existent nodes.
tx1Msg := wire.NewMsgTx(1)
tx2Msg := wire.NewMsgTx(1)
hash1 := tx1Msg.TxHash()
hash2 := tx2Msg.TxHash()
err := g.AddEdge(hash1, hash2)
require.Error(t, err)
require.Equal(t, ErrNodeNotFound, err)
// Add one node and try to add edge.
tx1, desc1 := createTestTx(nil, 1)
require.NoError(t, g.AddTransaction(tx1, desc1))
err = g.AddEdge(*tx1.Hash(), hash2)
require.Error(t, err)
require.Equal(t, ErrNodeNotFound, err)
// Add second node.
tx2, desc2 := createTestTx(nil, 1)
require.NoError(t, g.AddTransaction(tx2, desc2))
// Add valid edge.
err = g.AddEdge(*tx1.Hash(), *tx2.Hash())
require.NoError(t, err)
// Try to add duplicate edge.
err = g.AddEdge(*tx1.Hash(), *tx2.Hash())
require.NoError(t, err)
// Try to create cycle.
err = g.AddEdge(*tx2.Hash(), *tx1.Hash())
require.Error(t, err)
require.Equal(t, ErrCycleDetected, err)
}
// TestRemoveTransactionComplex tests complex removal scenarios.
func TestRemoveTransactionComplex(t *testing.T) {
g := New(DefaultConfig())

View file

@ -110,21 +110,6 @@ type PackageTopology struct {
IsTree bool
}
// TxEdge represents metadata about an edge.
type TxEdge struct {
// OutPoints identifies which specific outputs are being spent in this
// relationship, enabling detection of conflicts and double-spends.
OutPoints []wire.OutPoint
// Value tracks the total satoshi amount flowing through this edge,
// enabling economic analysis of transaction relationships.
Value int64
// Created records when this edge was established, useful for
// time-based analysis and debugging.
Created time.Time
}
// GraphMetrics provides statistics about the transaction graph.
type GraphMetrics struct {
// NodeCount tracks the total number of transactions in the graph for
@ -186,32 +171,6 @@ type TxGraphNode struct {
// scanning slices.
Children map[chainhash.Hash]*TxGraphNode
// cachedMetrics stores expensive-to-compute graph properties to avoid
// repeated traversals during policy checks. The cache is invalidated
// when ancestors or descendants change.
cachedMetrics struct {
// AncestorCount enables quick checks against BIP 125 limits.
AncestorCount int32
// DescendantCount enforces mempool policy limits efficiently.
DescendantCount int32
// AncestorSize tracks cumulative size for package limit checks.
AncestorSize int64
// DescendantSize enables fast descendant limit validation.
DescendantSize int64
// AncestorFees supports CPFP calculations.
AncestorFees int64
// DescendantFees enables descendant fee rate computations.
DescendantFees int64
// LastUpdated allows cache invalidation based on graph changes.
LastUpdated time.Time
}
// Metadata holds feature-specific flags and relationships that don't
// affect core graph structure but enable specialized processing.
Metadata struct {
@ -245,15 +204,6 @@ type TxCluster struct {
// O(1) membership tests during cluster merges and splits.
Nodes map[chainhash.Hash]*TxGraphNode
// Roots identifies transactions with no unconfirmed parents in this
// cluster. These are entry points for package evaluation and block
// template building.
Roots []*TxGraphNode
// Leaves identifies transactions with no children in this cluster.
// These are candidates for eviction when the mempool is full.
Leaves []*TxGraphNode
// Size tracks the number of transactions for quick cluster size checks
// without iterating the Nodes map.
Size int
@ -317,21 +267,6 @@ type TxPackage struct {
LastValidated time.Time
}
// EdgePair represents a parent-child relationship.
type EdgePair struct {
// Parent is the transaction being spent from, providing context for
// graph traversal and validation.
Parent *TxGraphNode
// Child is the transaction doing the spending, enabling forward
// traversal during descendant queries.
Child *TxGraphNode
// Edge contains metadata about the specific outputs being spent,
// enabling detailed analysis of fund flows.
Edge *TxEdge
}
// Graph defines the primary interface for transaction graph operations.
type Graph interface {
// AddTransaction inserts a new transaction into the graph and
@ -362,17 +297,6 @@ type Graph interface {
// data isn't needed.
HasTransaction(hash chainhash.Hash) bool
// AddEdge creates a parent-child relationship between two transactions
// that are already in the graph. This enables explicit edge management
// when transaction dependencies need to be added after initial
// insertion.
AddEdge(parent, child chainhash.Hash) error
// RemoveEdge severs a parent-child relationship without removing the
// transactions themselves. This is useful for handling reorganizations
// where relationships change but transactions remain valid.
RemoveEdge(parent, child chainhash.Hash) error
// GetAncestors returns all ancestor transactions up to maxDepth.
// This is used to enforce ancestor count/size limits for policy
// validation and to compute ancestor fee rates for CPFP.
@ -419,11 +343,6 @@ type Graph interface {
// without allocating memory for all matches upfront.
Iterate(opts IteratorOption) iter.Seq[*TxGraphNode]
// IteratePairs returns an iterator over parent-child edges in the
// graph. This enables efficient edge-based analysis like conflict
// detection and fund flow tracking.
IteratePairs(opts IteratorOption) iter.Seq[EdgePair]
// IteratePackages returns an iterator over all identified packages.
// This enables package-by-package processing during block template
// construction and relay decisions.
@ -566,112 +485,6 @@ func WithIncludeStart(include bool) IterOption {
}
}
// GraphQuery provides advanced query operations.
type GraphQuery interface {
// FindTransactions searches for transactions matching the specified
// criteria. This enables complex filtering operations like finding
// all TRUC transactions above a certain fee rate.
FindTransactions(criteria TxCriteria) []*TxGraphNode
// FindPackages searches for packages matching the specified criteria.
// This enables targeted package queries like finding all valid 1P1C
// packages above a minimum fee rate.
FindPackages(criteria PackageCriteria) []*TxPackage
// FindPath searches for a dependency path between two transactions.
// This is useful for understanding transaction relationships and
// debugging unexpected dependencies.
FindPath(from, to *chainhash.Hash) []*TxGraphNode
// HasPath checks if a dependency path exists without computing it.
// This enables efficient reachability checks for cycle detection and
// conflict analysis.
HasPath(from, to *chainhash.Hash) bool
// GetTopologicalOrder returns all transactions in topological order,
// ensuring parents appear before children. This is essential for block
// template construction where dependencies must be satisfied.
GetTopologicalOrder() []*TxGraphNode
// DetectCycles finds circular dependencies in the graph, which should
// never exist but can occur due to bugs. Each inner slice represents
// one cycle detected in the graph.
DetectCycles() [][]*TxGraphNode
// GetFeerateDistribution computes the cumulative feerate diagram for
// all transactions. This enables analysis of mempool composition and
// fee rate distributions.
GetFeerateDistribution() []FeeratePoint
// GetPackageFeerates computes the effective fee rate for each package.
// This enables package-based comparisons for relay and mining
// decisions.
GetPackageFeerates() map[PackageID]int64
}
// TxCriteria defines criteria for finding transactions.
type TxCriteria struct {
// MinFeeRate filters for transactions at or above this fee rate,
// enabling queries for high-priority transactions.
MinFeeRate int64
// MaxFeeRate filters for transactions at or above this fee rate,
// enabling queries for low-fee transactions that may need eviction.
MaxFeeRate int64
// MinSize filters for transactions at or above this size, useful for
// identifying large transactions that consume significant mempool
// space.
MinSize int64
// MaxSize filters for transactions at or below this size, useful for
// finding small transactions or enforcing size limits.
MaxSize int64
// IsTRUC filters by v3 transaction status. Nil means don't filter,
// true means only v3, false means only non-v3.
IsTRUC *bool
// IsEphemeral filters by ephemeral dust status. Nil means don't
// filter, enabling queries specific to ephemeral transactions.
IsEphemeral *bool
// HasAncestors filters by ancestor presence. Nil means don't filter,
// true finds transactions with parents, false finds root transactions.
HasAncestors *bool
// HasChildren filters by child presence. Nil means don't filter, true
// finds transactions with children, false finds leaf transactions.
HasChildren *bool
}
// PackageCriteria defines criteria for finding packages.
type PackageCriteria struct {
// Type filters by package type (1P1C, TRUC, ephemeral), enabling
// type-specific package queries.
Type PackageType
// MinSize filters for packages at or above this transaction count,
// useful for finding complex multi-transaction packages.
MinSize int
// MaxSize filters for packages at or below this transaction count,
// useful for finding simple packages or enforcing limits.
MaxSize int
// MinFeeRate filters for packages at or above this effective fee rate,
// enabling high-fee package identification.
MinFeeRate int64
// MaxFeeRate filters for packages at or below this effective fee rate,
// useful for low-fee package queries.
MaxFeeRate int64
// IsValid filters by validation status. Nil means don't filter,
// enabling queries for valid or invalid packages separately.
IsValid *bool
}
// InputConfirmedPredicate is a function that checks if a transaction input
// references a confirmed UTXO. This is used to distinguish between:
// - Orphans: transactions with unconfirmed inputs not in the mempool

View file

@ -4,7 +4,6 @@ import (
"iter"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
// Iterate returns an iterator over graph nodes.
@ -428,55 +427,6 @@ func (g *TxGraph) iterateFeeRate(
}
}
// IteratePairs returns an iterator over parent-child pairs.
func (g *TxGraph) IteratePairs(options ...IterOption) iter.Seq[EdgePair] {
// Build options with defaults.
opts := DefaultIteratorOption()
for _, option := range options {
option(&opts)
}
return func(yield func(EdgePair) bool) {
g.mu.RLock()
defer g.mu.RUnlock()
visited := make(map[string]bool) // Track visited edges
// Iterate directly over all nodes in the graph.
for _, node := range g.nodes {
// Apply filter if specified.
if opts.Filter != nil && !opts.Filter(node) {
continue
}
for _, child := range node.Children {
// Create unique edge key.
edgeKey := node.TxHash.String() + "->" + child.TxHash.String()
if visited[edgeKey] {
continue
}
visited[edgeKey] = true
// Create edge metadata.
edge := &TxEdge{
OutPoints: g.findOutpoints(node, child),
Created: node.Metadata.AddedTime,
}
pair := EdgePair{
Parent: node,
Child: child,
Edge: edge,
}
if !yield(pair) {
return
}
}
}
}
}
// IteratePackages returns an iterator over packages.
func (g *TxGraph) IteratePackages() iter.Seq[*TxPackage] {
return func(yield func(*TxPackage) bool) {
@ -637,17 +587,4 @@ func (g *TxGraph) addNeighborsToQueue(
}
}
}
}
// findOutpoints finds the outpoints connecting parent to child.
func (g *TxGraph) findOutpoints(parent, child *TxGraphNode) []wire.OutPoint {
var outpoints []wire.OutPoint
for _, txIn := range child.Tx.MsgTx().TxIn {
if txIn.PreviousOutPoint.Hash == parent.TxHash {
outpoints = append(outpoints, txIn.PreviousOutPoint)
}
}
return outpoints
}

View file

@ -242,42 +242,6 @@ func TestIteratorTraversalMethods(t *testing.T) {
})
}
// TestIteratePairs verifies that edge pair iteration produces all parent-
// child relationships in the graph. This is useful for analyzing spending
// patterns and computing aggregate statistics about transaction dependencies.
func TestIteratePairs(t *testing.T) {
g := New(DefaultConfig())
tx1, desc1 := createTestTx(nil, 1)
require.NoError(t, g.AddTransaction(tx1, desc1))
tx2, desc2 := createTestTx(
[]wire.OutPoint{{Hash: *tx1.Hash(), Index: 0}}, 1,
)
require.NoError(t, g.AddTransaction(tx2, desc2))
tx3, desc3 := createTestTx(
[]wire.OutPoint{{Hash: *tx2.Hash(), Index: 0}}, 1,
)
require.NoError(t, g.AddTransaction(tx3, desc3))
edges := make(map[string]bool)
for pair := range g.IteratePairs() {
edgeKey := pair.Parent.TxHash.String() + "->" +
pair.Child.TxHash.String()
edges[edgeKey] = true
// Each edge pair should include metadata about which outputs
// are being spent, enabling detailed dependency analysis.
require.NotNil(t, pair.Edge)
require.NotEmpty(t, pair.Edge.OutPoints)
}
require.Len(t, edges, 2)
require.True(t, edges[tx1.Hash().String()+"->"+tx2.Hash().String()])
require.True(t, edges[tx2.Hash().String()+"->"+tx3.Hash().String()])
}
// TestIteratePackages verifies that package iteration produces all
// identified transaction packages in the graph. Package iteration enables
// efficient processing of transaction groups for package relay policies and

View file

@ -259,97 +259,6 @@ func TestIterateWithDirectionBoth(t *testing.T) {
)
}
// TestIteratePairsWithOptions validates that IteratePairs correctly emits
// parent-child relationships as pairs, which is essential for CPFP (Child
// Pays For Parent) analysis. By iterating edges rather than nodes, we can
// efficiently compute fee deltas and determine which children are boosting
// low-fee ancestors.
func TestIteratePairsWithOptions(t *testing.T) {
g := New(DefaultConfig())
// Build a tree with one parent and two children to test edge
// enumeration.
tx1, desc1 := createTestTx(nil, 2)
require.NoError(t, g.AddTransaction(tx1, desc1))
tx2, desc2 := createTestTx(
[]wire.OutPoint{{Hash: *tx1.Hash(), Index: 0}}, 1,
)
require.NoError(t, g.AddTransaction(tx2, desc2))
tx3, desc3 := createTestTx(
[]wire.OutPoint{{Hash: *tx1.Hash(), Index: 1}}, 1,
)
require.NoError(t, g.AddTransaction(tx3, desc3))
// IteratePairs emits one pair per edge, allowing us to analyze
// each parent-child relationship independently for fee rate
// calculations.
pairs := slices.Collect(g.IteratePairs(
WithOrder(TraversalDefault),
WithStartNode(tx1.Hash()),
WithDirection(DirectionForward),
))
require.Len(t, pairs, 2, "should have 2 edges from tx1")
// Verify each pair represents a valid edge from tx1 to one of its
// children.
for _, pair := range pairs {
require.Equal(t, *tx1.Hash(), pair.Parent.TxHash)
require.True(t,
pair.Child.TxHash == *tx2.Hash() ||
pair.Child.TxHash == *tx3.Hash(),
"child should be tx2 or tx3",
)
}
}
// TestIteratePairsWithFilter validates that filters are applied to
// parent-child pairs, enabling selective analysis of specific
// relationships. This is used in RBF (Replace-By-Fee) scenarios where we
// need to identify which high-value dependencies would be broken by
// replacing a transaction.
func TestIteratePairsWithFilter(t *testing.T) {
g := New(DefaultConfig())
// Create two independent parent-child chains with different fee
// rates to test filtering at the edge level.
tx1, desc1 := createTestTx(nil, 1)
desc1.FeePerKB = 10000
require.NoError(t, g.AddTransaction(tx1, desc1))
tx2, desc2 := createTestTx(nil, 1)
desc2.FeePerKB = 1000
require.NoError(t, g.AddTransaction(tx2, desc2))
tx3, desc3 := createTestTx(
[]wire.OutPoint{{Hash: *tx1.Hash(), Index: 0}}, 1,
)
require.NoError(t, g.AddTransaction(tx3, desc3))
tx4, desc4 := createTestTx(
[]wire.OutPoint{{Hash: *tx2.Hash(), Index: 0}}, 1,
)
require.NoError(t, g.AddTransaction(tx4, desc4))
// The filter applies to parent nodes in the pairs, allowing us to
// focus analysis on edges originating from high-fee transactions.
highFeeFilter := func(n *TxGraphNode) bool {
return n.TxDesc.FeePerKB >= 5000
}
pairs := slices.Collect(g.IteratePairs(
WithOrder(TraversalDefault),
WithFilter(highFeeFilter),
))
// Only the edge from high-fee tx1 should appear.
require.Len(t, pairs, 1, "should filter out low-fee parent edges")
require.Equal(t, *tx1.Hash(), pairs[0].Parent.TxHash)
require.Equal(t, *tx3.Hash(), pairs[0].Child.TxHash)
}
// TestIterateBackwardWithMaxDepth ensures that depth limits correctly
// bound backward traversal. This prevents unbounded ancestor walks in
// large transaction chains and enables efficient "bounded ancestor