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.
This commit is contained in:
Olaoluwa Osuntokun 2025-10-01 15:19:39 -07:00
parent af3e8d2352
commit 1cdd76fc50
2 changed files with 98 additions and 0 deletions

View file

@ -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 {

View file

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