From 1cdd76fc50364911a4eef885eacfe8a4969a2aa5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 1 Oct 2025 15:19:39 -0700 Subject: [PATCH] mempool/txgraph: add GetConflicts for RBF conflict detection Add GetConflicts method to detect which mempool transactions would be replaced by a new transaction. Returns both individual conflicting transactions and their packages to support package-based eviction. Uses the spentBy index for O(1) conflict lookups per input. --- mempool/txgraph/graph.go | 70 +++++++++++++++++++++++++++++++++++ mempool/txgraph/interfaces.go | 28 ++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/mempool/txgraph/graph.go b/mempool/txgraph/graph.go index 3c7586c0..731c0284 100644 --- a/mempool/txgraph/graph.go +++ b/mempool/txgraph/graph.go @@ -556,6 +556,76 @@ func (g *TxGraph) GetOrphans(isConfirmed InputConfirmedPredicate, return slices.Collect(g.IterateOrphans(isConfirmed)) } +// GetConflicts returns all transactions and packages that conflict with the +// given transaction. A conflict occurs when the input transaction attempts to +// spend an output that is already spent by a transaction in the mempool. +// +// The method uses the spentBy index for O(1) conflict detection per input. +// For each conflicting transaction found, it includes all descendants since +// they would become invalid if their ancestor is replaced. It also identifies +// any packages that contain conflicting transactions. +// +// The returned ConflictSet provides both individual transactions (for +// fine-grained analysis) and packages (for package-based eviction policies). +// +// Returns an empty ConflictSet if there are no conflicts. +func (g *TxGraph) GetConflicts(tx *btcutil.Tx) *ConflictSet { + g.mu.RLock() + defer g.mu.RUnlock() + + result := &ConflictSet{ + Transactions: make(map[chainhash.Hash]*TxGraphNode), + Packages: make(map[PackageID]*TxPackage), + } + + // Check each input of the candidate transaction for conflicts with + // existing mempool transactions. The spentBy index maps each spent + // output to the transaction that spends it, enabling O(1) lookups. + for _, txIn := range tx.MsgTx().TxIn { + // If this outpoint is already spent by a mempool transaction, + // that transaction conflicts with our candidate. + conflictNode, exists := g.indexes.spentBy[txIn.PreviousOutPoint] + if !exists { + continue + } + + // Add the directly conflicting transaction. + result.Transactions[conflictNode.TxHash] = conflictNode + + // Add all descendants of the conflict. When we replace a + // transaction via RBF, all its descendants must also be + // removed because they spend outputs that will no longer + // exist. + descendants := g.GetDescendants(conflictNode.TxHash, -1) + for hash, node := range descendants { + result.Transactions[hash] = node + } + } + + // Identify packages that contain any conflicting transactions. This + // enables package-based eviction where entire packages are treated as + // atomic units. + for hash := range result.Transactions { + pkgID, exists := g.indexes.nodeToPackage[hash] + if !exists { + continue + } + + // Only add each package once even if multiple transactions + // from the same package are in the conflict set. + if _, alreadyAdded := result.Packages[pkgID]; alreadyAdded { + continue + } + + pkg, exists := g.indexes.packages[pkgID] + if exists { + result.Packages[pkgID] = pkg + } + } + + return result +} + // ValidatePackage validates a transaction package. func (g *TxGraph) ValidatePackage(pkg *TxPackage) error { if pkg == nil { diff --git a/mempool/txgraph/interfaces.go b/mempool/txgraph/interfaces.go index 325eebfb..0873826a 100644 --- a/mempool/txgraph/interfaces.go +++ b/mempool/txgraph/interfaces.go @@ -372,6 +372,16 @@ type Graph interface { // indicates mempool fragmentation and is useful for understanding the // effectiveness of cluster-based optimizations. GetClusterCount() int + + // GetConflicts returns all transactions and packages that would be + // replaced if the given transaction were added to the mempool. A + // conflict occurs when a transaction input spends an output that is + // already spent by a transaction in the graph. The returned ConflictSet + // includes both directly conflicting transactions and all their + // descendants, since descendants become invalid when their ancestor is + // replaced. It also includes any packages that contain conflicting + // transactions, enabling package-based eviction policies. + GetConflicts(tx *btcutil.Tx) *ConflictSet } // TraversalOrder defines the traversal strategy for graph iteration. @@ -485,6 +495,24 @@ func WithIncludeStart(include bool) IterOption { } } +// ConflictSet contains the result of a conflict check, providing both +// individual conflicting transactions and their associated packages. +type ConflictSet struct { + // Transactions contains all conflicting transactions and their + // descendants as a flat map. This enables direct iteration and + // individual transaction analysis. + Transactions map[chainhash.Hash]*TxGraphNode + + // Packages contains all packages that include at least one conflicting + // transaction. This enables package-based eviction policies where entire + // packages are considered as atomic units. + // + // Note: Not all conflicting transactions are necessarily in packages. + // Standalone transactions will appear in Transactions but not in + // Packages. + Packages map[PackageID]*TxPackage +} + // 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