Skip to content

Commit 1930f49

Browse files
committed
mempool: expand PolicyEnforcer interface for complete validation
In this commit, we expand the PolicyEnforcer interface to include methods for validating standardness, signature costs, and SegWit deployment. This makes PolicyEnforcer a complete abstraction for all policy-level validation, complementing the TxValidator's consensus-level checks. We add four new methods to the PolicyEnforcer interface: ValidateStandardness checks transaction standardness requirements including version, finalization, size limits, script types, and dust outputs. This delegates to the existing CheckTransactionStandard and checkInputsStandard helper functions. ValidateSigCost validates that signature operation cost doesn't exceed relay limits, using the CheckTransactionSigCost helper function we extracted earlier. ValidateSegWitDeployment ensures SegWit transactions are only accepted when SegWit is active, using CheckSegWitDeployment. ValidateRelayFee signature is updated to accept utxoView and nextBlockHeight, enabling priority calculation for low-fee transactions. The implementation now calls CheckRelayFee for the core validation, then applies rate limiting separately. The PolicyConfig structure is expanded to include MaxTxVersion, MaxSigOpCostPerTx, IsDeploymentActive, ChainParams, and BestHeight. These fields enable the policy enforcer to perform validation without requiring external context from the mempool. DefaultPolicyConfig provides sensible defaults for all new fields, assuming SegWit is active and using mainnet parameters. StandardPolicyEnforcer implements all new methods by delegating to the standalone helper functions in policy.go. This keeps the implementation thin while maintaining full test coverage through the existing helper function tests. The ValidateRelayFee tests are updated to pass the new parameters. We fix a test case that was incorrectly expecting non-new transactions with low fees to be rejected - per the CheckRelayFee implementation, non-new transactions (from reorgs) are exempted from priority checks for transactions under the size threshold. We add a new test case showing that large non-new transactions are still correctly rejected if their fees are insufficient. This expanded interface allows TxMempoolV2's validation pipeline to use PolicyEnforcer for all policy checks, creating a clean separation between consensus validation (TxValidator) and policy validation (PolicyEnforcer).
1 parent 90edbb1 commit 1930f49

File tree

2 files changed

+152
-38
lines changed

2 files changed

+152
-38
lines changed

mempool/policy_enforcer.go

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"sync"
1212
"time"
1313

14+
"github.com/btcsuite/btcd/blockchain"
1415
"github.com/btcsuite/btcd/btcutil"
16+
"github.com/btcsuite/btcd/chaincfg"
1517
"github.com/btcsuite/btcd/chaincfg/chainhash"
1618
"github.com/btcsuite/btcd/mempool/txgraph"
1719
)
@@ -54,22 +56,24 @@ type PolicyGraph interface {
5456
// GetNode retrieves a transaction node from the graph.
5557
GetNode(hash chainhash.Hash) (*txgraph.TxGraphNode, bool)
5658

57-
// GetAncestors returns all ancestor transactions up to maxDepth.
58-
// A negative maxDepth returns all ancestors.
59-
GetAncestors(hash chainhash.Hash, maxDepth int) map[chainhash.Hash]*txgraph.TxGraphNode
59+
// GetAncestors returns all ancestor transactions up to maxDepth. A
60+
// negative maxDepth returns all ancestors.
61+
GetAncestors(hash chainhash.Hash,
62+
maxDepth int) map[chainhash.Hash]*txgraph.TxGraphNode
6063

61-
// GetDescendants returns all descendant transactions up to maxDepth.
62-
// A negative maxDepth returns all descendants.
63-
GetDescendants(hash chainhash.Hash, maxDepth int) map[chainhash.Hash]*txgraph.TxGraphNode
64+
// GetDescendants returns all descendant transactions up to maxDepth. A
65+
// negative maxDepth returns all descendants.
66+
GetDescendants(hash chainhash.Hash,
67+
maxDepth int) map[chainhash.Hash]*txgraph.TxGraphNode
6468
}
6569

6670
// PolicyEnforcer defines the interface for mempool policy enforcement. This
6771
// separates policy decisions from graph data structure operations, enabling
6872
// easier testing and different policy configurations.
6973
type PolicyEnforcer interface {
70-
// SignalsReplacement determines if a transaction signals that it can be
71-
// replaced using the Replace-By-Fee (RBF) policy. This includes both
72-
// explicit signaling (sequence number) and inherited signaling
74+
// SignalsReplacement determines if a transaction signals that it can
75+
// be replaced using the Replace-By-Fee (RBF) policy. This includes
76+
// both explicit signaling (sequence number) and inherited signaling
7377
// (unconfirmed ancestors that signal RBF).
7478
SignalsReplacement(graph PolicyGraph, tx *btcutil.Tx) bool
7579

@@ -87,9 +91,26 @@ type PolicyEnforcer interface {
8791
ValidateDescendantLimits(graph PolicyGraph, hash chainhash.Hash) error
8892

8993
// ValidateRelayFee checks that a transaction meets the minimum relay
90-
// fee requirements, including rate limiting for free transactions.
94+
// fee requirements, including priority checks and rate limiting for
95+
// free/low-fee transactions.
9196
ValidateRelayFee(tx *btcutil.Tx, fee int64, size int64,
97+
utxoView *blockchain.UtxoViewpoint, nextBlockHeight int32,
9298
isNew bool) error
99+
100+
// ValidateStandardness checks that a transaction meets standardness
101+
// requirements for relay (version, size, scripts, dust outputs).
102+
ValidateStandardness(tx *btcutil.Tx, height int32,
103+
medianTimePast time.Time, utxoView *blockchain.UtxoViewpoint,
104+
) error
105+
106+
// ValidateSigCost checks that a transaction's signature operation cost
107+
// does not exceed the maximum allowed for relay.
108+
ValidateSigCost(tx *btcutil.Tx,
109+
utxoView *blockchain.UtxoViewpoint) error
110+
111+
// ValidateSegWitDeployment checks that if a transaction contains
112+
// witness data, the SegWit soft fork must be active.
113+
ValidateSegWitDeployment(tx *btcutil.Tx) error
93114
}
94115

95116
// PolicyConfig defines mempool policy parameters. These settings control
@@ -136,6 +157,25 @@ type PolicyConfig struct {
136157
// DisableRelayPriority, if true, disables relaying of low-fee
137158
// transactions based on priority.
138159
DisableRelayPriority bool
160+
161+
// MaxTxVersion is the maximum transaction version to accept.
162+
// Transactions with versions above this are rejected as non-standard.
163+
MaxTxVersion int32
164+
165+
// MaxSigOpCostPerTx is the cumulative maximum cost of all signature
166+
// operations in a single transaction that will be relayed or mined.
167+
MaxSigOpCostPerTx int
168+
169+
// IsDeploymentActive checks if a consensus deployment is active.
170+
// This is used for validating SegWit transactions.
171+
IsDeploymentActive func(deploymentID uint32) (bool, error)
172+
173+
// ChainParams identifies the blockchain network (mainnet, testnet,
174+
// etc). Used for network-specific validation rules.
175+
ChainParams *chaincfg.Params
176+
177+
// BestHeight returns the current best block height.
178+
BestHeight func() int32
139179
}
140180

141181
// DefaultPolicyConfig returns a PolicyConfig with default values matching
@@ -157,6 +197,23 @@ func DefaultPolicyConfig() PolicyConfig {
157197
MinRelayTxFee: DefaultMinRelayTxFee,
158198
FreeTxRelayLimit: 15.0,
159199
DisableRelayPriority: false,
200+
201+
// Transaction version and signature operation limits.
202+
MaxTxVersion: 2, // Standard transaction version
203+
MaxSigOpCostPerTx: 80000, // 1/5 of max block sigop cost
204+
205+
// Default deployment check (assume SegWit is active for testing).
206+
IsDeploymentActive: func(deploymentID uint32) (bool, error) {
207+
return true, nil
208+
},
209+
210+
// Default to mainnet params.
211+
ChainParams: &chaincfg.MainNetParams,
212+
213+
// Default height (reasonable for testing).
214+
BestHeight: func() int32 {
215+
return 700000
216+
},
160217
}
161218
}
162219

@@ -390,9 +447,7 @@ func (p *StandardPolicyEnforcer) ValidateAncestorLimits(
390447
// size to prevent unbounded chain growth in the mempool. This implementation
391448
// matches that behavior.
392449
func (p *StandardPolicyEnforcer) ValidateDescendantLimits(
393-
graph PolicyGraph,
394-
hash chainhash.Hash,
395-
) error {
450+
graph PolicyGraph, hash chainhash.Hash) error {
396451

397452
// Get all descendants for this transaction.
398453
descendants := graph.GetDescendants(hash, -1)
@@ -421,29 +476,34 @@ func (p *StandardPolicyEnforcer) ValidateDescendantLimits(
421476
}
422477

423478
// ValidateRelayFee checks that a transaction meets the minimum relay fee
424-
// requirements, including rate limiting for free/low-fee transactions.
425-
//
426-
// Transactions with fees below the minimum are rate-limited using an
427-
// exponentially decaying counter to prevent spam while allowing some free
479+
// requirements, including priority checks and rate limiting for free/low-fee
428480
// transactions.
481+
//
482+
// Transactions with fees below the minimum are checked for priority (if
483+
// enabled) and rate-limited using an exponentially decaying counter to prevent
484+
// spam while allowing some free transactions.
429485
func (p *StandardPolicyEnforcer) ValidateRelayFee(
430-
tx *btcutil.Tx, fee int64, size int64, isNew bool) error {
486+
tx *btcutil.Tx, fee int64, size int64, utxoView *blockchain.UtxoViewpoint,
487+
nextBlockHeight int32, isNew bool) error {
488+
489+
// First check the minimum relay fee and priority requirements using
490+
// the standalone CheckRelayFee function.
491+
err := CheckRelayFee(
492+
tx, fee, size, utxoView, nextBlockHeight,
493+
p.cfg.MinRelayTxFee, p.cfg.DisableRelayPriority, isNew,
494+
)
495+
if err != nil {
496+
return err
497+
}
431498

432-
// Calculate minimum required fee for this transaction.
499+
// Calculate minimum required fee to determine if rate limiting applies.
433500
minFee := calcMinRequiredTxRelayFee(size, p.cfg.MinRelayTxFee)
434501

435-
// If the fee meets the minimum, accept it immediately.
502+
// If the fee meets the minimum, no rate limiting needed.
436503
if fee >= minFee {
437504
return nil
438505
}
439506

440-
// If this is not a new transaction or if relay priority is disabled,
441-
// reject it for insufficient fee.
442-
if !isNew || p.cfg.DisableRelayPriority {
443-
return fmt.Errorf("transaction %v has insufficient fee: %d < %d",
444-
tx.Hash(), fee, minFee)
445-
}
446-
447507
// Apply rate limiting for free/low-fee transactions.
448508
p.mu.Lock()
449509
defer p.mu.Unlock()
@@ -469,5 +529,46 @@ func (p *StandardPolicyEnforcer) ValidateRelayFee(
469529
return nil
470530
}
471531

532+
// ValidateStandardness checks that a transaction meets standardness
533+
// requirements for relay. This includes version checks, finalization, size
534+
// limits, script checks, and dust checks.
535+
func (p *StandardPolicyEnforcer) ValidateStandardness(
536+
tx *btcutil.Tx, height int32, medianTimePast time.Time,
537+
utxoView *blockchain.UtxoViewpoint) error {
538+
539+
// Use the existing CheckTransactionStandard function which handles
540+
// version, finalization, size, and output script checks.
541+
err := CheckTransactionStandard(
542+
tx, height, medianTimePast,
543+
p.cfg.MinRelayTxFee, p.cfg.MaxTxVersion,
544+
)
545+
if err != nil {
546+
return err
547+
}
548+
549+
// Also check input standardness (signature scripts, etc).
550+
return checkInputsStandard(tx, utxoView)
551+
}
552+
553+
// ValidateSigCost checks that a transaction's signature operation cost does
554+
// not exceed the maximum allowed for relay.
555+
func (p *StandardPolicyEnforcer) ValidateSigCost(
556+
tx *btcutil.Tx, utxoView *blockchain.UtxoViewpoint) error {
557+
558+
// Use the standalone CheckTransactionSigCost function to validate.
559+
return CheckTransactionSigCost(tx, utxoView, p.cfg.MaxSigOpCostPerTx)
560+
}
561+
562+
// ValidateSegWitDeployment checks that if a transaction contains witness data,
563+
// the SegWit soft fork must be active.
564+
func (p *StandardPolicyEnforcer) ValidateSegWitDeployment(tx *btcutil.Tx) error {
565+
566+
// Use the standalone CheckSegWitDeployment function to validate.
567+
return CheckSegWitDeployment(
568+
tx, p.cfg.IsDeploymentActive, p.cfg.ChainParams,
569+
p.cfg.BestHeight(),
570+
)
571+
}
572+
472573
// Ensure StandardPolicyEnforcer implements PolicyEnforcer interface.
473574
var _ PolicyEnforcer = (*StandardPolicyEnforcer)(nil)

mempool/policy_enforcer_test.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -591,16 +591,29 @@ func TestValidateRelayFee(t *testing.T) {
591591
minFee := calcMinRequiredTxRelayFee(size, cfg.MinRelayTxFee)
592592

593593
// Test with sufficient fee.
594-
err := p.ValidateRelayFee(tx, minFee, size, true)
594+
err := p.ValidateRelayFee(tx, minFee, size, nil, 0, true)
595595
require.NoError(t, err, "should accept transaction with sufficient fee")
596596

597597
// Test with insufficient fee (should be rate limited for new tx).
598-
err = p.ValidateRelayFee(tx, minFee-1, size, true)
598+
err = p.ValidateRelayFee(tx, minFee-1, size, nil, 0, true)
599599
require.NoError(t, err, "should accept new low-fee tx (rate limited)")
600600

601-
// Test with insufficient fee for non-new tx.
602-
err = p.ValidateRelayFee(tx, minFee-1, size, false)
603-
require.Error(t, err, "should reject non-new low-fee tx")
601+
// Test with insufficient fee for non-new tx (reorg scenario).
602+
// Non-new transactions are exempted from priority checks per the
603+
// CheckRelayFee implementation (line 469-472 in policy.go), so they
604+
// should be allowed even with low fees as long as they're under the
605+
// size threshold (DefaultBlockPrioritySize - 1000 = 49000 bytes).
606+
err = p.ValidateRelayFee(tx, minFee-1, size, nil, 0, false)
607+
require.NoError(t, err, "should accept non-new low-fee tx (reorg exemption)")
608+
609+
// Test with a large transaction that exceeds the free transaction
610+
// size threshold. Even non-new transactions should be rejected if
611+
// they're too large and have insufficient fees.
612+
largeSize := int64(DefaultBlockPrioritySize - 500) // 49500 bytes
613+
largeTxMinFee := calcMinRequiredTxRelayFee(largeSize, cfg.MinRelayTxFee)
614+
err = p.ValidateRelayFee(tx, largeTxMinFee-1, largeSize, nil, 0, false)
615+
require.Error(t, err, "should reject large non-new tx with insufficient fee")
616+
require.Contains(t, err.Error(), "under the required amount")
604617
}
605618

606619
// TestRateLimiting tests that the rate limiter correctly limits free
@@ -616,11 +629,11 @@ func TestRateLimiting(t *testing.T) {
616629
size := GetTxVirtualSize(tx)
617630

618631
// First free tx should be accepted.
619-
err := p.ValidateRelayFee(tx, 0, size, true)
632+
err := p.ValidateRelayFee(tx, 0, size, nil, 0, true)
620633
require.NoError(t, err)
621634

622635
// Second free tx should be rejected (rate limited).
623-
err = p.ValidateRelayFee(tx, 0, size, true)
636+
err = p.ValidateRelayFee(tx, 0, size, nil, 0, true)
624637
require.Error(t, err)
625638
require.Contains(t, err.Error(), "rate limiter")
626639
}
@@ -638,7 +651,7 @@ func TestRateLimiterDecay(t *testing.T) {
638651
size := int64(50)
639652

640653
// Accept first transaction.
641-
err := p.ValidateRelayFee(tx, 0, size, true)
654+
err := p.ValidateRelayFee(tx, 0, size, nil, 0, true)
642655
require.NoError(t, err)
643656

644657
// Manually advance time to simulate decay.
@@ -650,7 +663,7 @@ func TestRateLimiterDecay(t *testing.T) {
650663
p.mu.Unlock()
651664

652665
// Second transaction should be accepted after decay.
653-
err = p.ValidateRelayFee(tx, 0, size, true)
666+
err = p.ValidateRelayFee(tx, 0, size, nil, 0, true)
654667
require.NoError(t, err)
655668
}
656669

@@ -817,8 +830,8 @@ func TestPropertyFeeRateMonotonic(t *testing.T) {
817830
tx := createTxWithSequence([]uint32{wire.MaxTxInSequenceNum})
818831

819832
// Higher fee should always be accepted if lower fee is accepted.
820-
err1 := p.ValidateRelayFee(tx, fee1, size, false)
821-
err2 := p.ValidateRelayFee(tx, fee2, size, false)
833+
err1 := p.ValidateRelayFee(tx, fee1, size, nil, 0, false)
834+
err2 := p.ValidateRelayFee(tx, fee2, size, nil, 0, false)
822835

823836
if err1 == nil && err2 != nil {
824837
t.Fatalf("monotonicity violated: fee %d accepted but %d rejected",

0 commit comments

Comments
 (0)