Skip to content

Conversation

@Roasbeef
Copy link
Member

In this PR, we implement BIP 431, building on all the prior PRs in this series.

As a temp set of commits, we've merged the v3 transaction awareness into this branch. These commits will be dropped once those changes land.

Key Changes

BIP 431 TRUC Validation

The StandardPackageAnalyzer implementation enforces all six BIP 431 rules for v3 transactions. The analyzer validates the all-or-none requirement that prevents mixing v2 and v3 transactions in unconfirmed chains, enforces the 1-parent-1-child topology limit to prevent pinning attacks, applies appropriate size constraints (10,000 vB for parents, 1,000 vB for children), and allows zero-fee parents in packages for CPFP scenarios. The validation logic is abstracted through the PackageAnalyzer interface.

Topology Calculation Fix

The package topology calculation had a critical bug where MaxDepth was incorrectly computed for packages containing temporary validation nodes. During pre-acceptance validation, we create a temporary TxGraphNode to check policy before committing to AddTransaction. However, the BFS traversal used node.Children maps, and temporary nodes aren't in these maps yet since they haven't been added to the graph. This caused three-transaction chains to show MaxDepth=1 instead of MaxDepth=2, allowing invalid TRUC chains that should have been rejected.

The fix builds a package-local Children map by examining Parent relationships before performing BFS. This ensures the traversal can reach temporary nodes and correctly compute topology metrics even when those nodes aren't yet in the graph's persistent structure. The implementation also refactors to use proper Queue generics and early return patterns for better code clarity.

NOTE: I'll likely commit this bug fix into the PR in the chain that added the analyzer in the first place.

Package Tracking Infrastructure

Packages are now automatically tracked in the graph's indexes when transactions are added, but validation uses an explicit dry run mode to avoid tracking packages before AddTransaction succeeds. This prevents index inconsistency if validation passes but addition fails for other reasons. The functional options pattern with WithDryRun() makes the tracking behavior explicit at call sites.

When new children are added to existing parents, the old parent package is invalidated and a new package with updated topology is tracked. Package removal properly cleans up indexes when transactions leave the graph. This ensures package state always remains consistent with graph state throughout the transaction lifecycle.

Architectural Integration

Package validation is integrated into PolicyEnforcer rather than being handled directly by the mempool orchestration layer. The TxGraph.IsValidPackageExtension method provides a composite interface that encapsulates the validation pattern, and PolicyEnforcer.ValidatePackagePolicy delegates to this capability. This follows the same pattern as other policy methods like ValidateAncestorLimits, maintaining consistency across the policy enforcement layer.

The separation keeps the mempool focused on orchestration while PolicyEnforcer owns all policy decisions. Package policy validation (TRUC topology, ephemeral dust) lives alongside transaction policy validation (fees, standardness, ancestors), providing a unified policy layer.

This commit introduces the MaxStandardTxVersion constant to the mempool
policy package, setting it to 3 to enable support for version 3
transactions. Previously, the maximum accepted transaction version was
hardcoded to 2 in the server configuration.

Note that we don't yet implement the new TRUC rules, yet. But this'll
enable acceptance into the btcd mempool.
In this commit, we replace the hardcoded transaction version limit of 2
with the MaxStandardTxVersion constant from the mempool package. This
change ensures the server configuration stays synchronized with the
mempool policy definition and enables v3 transaction support throughout
the system.
In this commit, we introduce a functional options pattern to the test
memwallet, allowing tests to specify custom transaction versions when
creating transactions. This enhancement provides the test infrastructure
needed to validate v3 transaction handling throughout the system.

All transaction creation methods (CreateTransaction, SendOutputs, and
SendOutputsWithoutChange) now accept variadic options while maintaining
backward compatibility - existing tests continue to work unchanged as
the default behavior remains version 1 transactions.
In this commit, we extend the mempool policy test suite to explicitly
verify that version 2 and version 3 transactions are accepted as
standard. The test now uses the MaxStandardTxVersion constant rather
than a hardcoded value, ensuring the tests stay synchronized with the
actual policy implementation.

We added specific test cases for v2 and v3 transactions to document and
verify the expected behavior, confirming that transactions with these
versions pass the standardness checks. The test for excessive version
numbers was also updated to use MaxStandardTxVersion + 1, making it
clear that we're testing the rejection of versions beyond our current
policy limit.
In this commit, we introduce a comprehensive integration test suite that
validates v3 transaction handling across the full stack, from mempool
acceptance through mining into blocks. These tests ensure that v3
transactions work correctly in real-world scenarios using the RPC test
harness.

The test suite covers multiple critical scenarios to ensure robust v3
transaction support. TestV3TransactionPolicy validates that transactions
with versions 1, 2, and 3 are accepted while version 4 is properly
rejected, confirming our policy enforcement works as intended. The test
also verifies that accepted v3 transactions can be successfully mined
into blocks, demonstrating end-to-end functionality.

TestV3TransactionRPCSubmission specifically tests the RPC pathway,
ensuring that v3 transactions created with the WithTxVersion option can
be submitted through SendOutputs and properly confirmed in blocks. This
validates that the functional options we added to the test
infrastructure
work correctly in practice.

TestV3TransactionChaining goes further by testing that outputs from v3
transactions can be spent by subsequent transactions, confirming that
the UTXO management and transaction validation logic properly handles
the new version throughout the transaction lifecycle.
This commit adds a concrete implementation of the PackageAnalyzer interface
to enforce TRUC (Topologically Restricted Until Confirmation) validation
rules per BIP 431. The analyzer provides the policy logic for validating
v3 transaction packages, which is essential for Lightning Network and other
contracting protocols that require reliable fee-bumping.

The StandardPackageAnalyzer implements all BIP 431 rules:

Rule 1 (signaling): v3 transactions signal replaceability - handled in
PolicyEnforcer's SignalsReplacement method.

Rule 2 (all-or-none): v3 transactions cannot have v2 unconfirmed ancestors
or descendants. This prevents bypassing TRUC topology limits through mixed
version chains.

Rule 3 (topology limits): Enforces 1-parent-1-child topology by checking
package MaxDepth ≤ 1. This prevents transaction pinning through long
unconfirmed chains.

Rule 4 (parent size): TRUC parents without unconfirmed ancestors must be
≤ 10,000 vB to prevent excessively large transactions while allowing
sufficient space for Lightning commitment transactions and HTLCs.

Rule 5 (child size): TRUC children with unconfirmed ancestors must be
≤ 1,000 vB to keep fee-bumping economical while allowing space for
fee-bumping inputs.

Rule 6 (zero-fee): Zero-fee TRUC parents are allowed in packages if the
package meets minimum fee requirements. This enables CPFP fee-bumping
patterns.
This commit addresses a critical bug in package topology calculation and
implements comprehensive package tracking infrastructure for the transaction
graph.

The primary bug fix resolves an issue where MaxDepth was incorrectly
calculated for packages containing temporary nodes (transactions not yet
added to the graph). During validation, we create a temporary TxGraphNode
to check package policy before committing to AddTransaction. However, the
topology calculation's BFS traversal relied on node.Children maps, and
temporary nodes aren't in any Children maps yet since they haven't been
added. This caused three-transaction chains (grandparent→parent→child) to
show MaxDepth=1 instead of MaxDepth=2, allowing invalid TRUC chains to be
accepted when they should have been rejected for violating BIP 431's
ancestor limit.

The fix builds a package-local Children map by examining the Parents
relationships of all nodes in the package before performing BFS. This
ensures the traversal can reach temporary nodes and correctly compute depth
even when those nodes aren't yet in the graph's persistent Children maps.
The implementation also switches from an anonymous struct slice to the
proper Queue generic for cleaner, more maintainable BFS code.

Beyond the topology fix, this commit implements automatic package tracking.
Previously, validated packages were ephemeral and discarded after
validation. Now packages are automatically tracked in the graph's indexes
when AddTransaction succeeds, but NOT during validation (when temp nodes
exist). This is achieved through a functional options pattern with
WithDryRun() that explicitly controls tracking behavior. The CreatePackage
method accepts PackageOption parameters, and maybeTrackPackage only indexes
packages when all nodes are persistent in the graph.

Package tracking happens automatically in AddTransaction via
trackNodePackage, which constructs the package from the newly added node,
its parents, and siblings. When new children are added to existing parents,
invalidatePackagesContaining removes the stale parent package before
tracking the updated topology. Package removal is handled in
removeTransactionUnsafe by calling invalidatePackagesContaining, ensuring
indexes stay consistent when transactions leave the graph.
This commit moves package policy validation into the PolicyEnforcer layer,
providing better architectural separation between the mempool orchestration
layer and policy enforcement logic. Previously, the mempool directly called
graph methods (BuildPackageNodes + CreatePackage) to validate packages. Now
validation is encapsulated in PolicyEnforcer, hiding implementation details
and providing a cleaner interface.

In mempool_v2, the validation pipeline now calls
policy.ValidatePackagePolicy(graph, tx, desc) instead of directly
orchestrating graph operations. This simplifies the mempool's
responsibility to pure orchestration while PolicyEnforcer owns all policy
decisions. The validation_v2 integration ensures package policy checks occur
at the appropriate point in the acceptance pipeline, after standard
transaction validation but before adding to the graph.

The changes maintain backward compatibility with all existing tests passing.
Package validation still uses WithDryRun() mode during pre-acceptance
checks to avoid tracking packages for transactions that haven't been
committed yet. Automatic tracking happens only after AddTransaction
succeeds, ensuring package indexes remain consistent with graph state.
This commit integrates the new package validation infrastructure into the
server's transaction handling pipeline. When mempool v2 is enabled, the
server now properly enforces BIP 431 TRUC topology constraints and other
package-level policies during transaction acceptance.

The change ensures that when transactions are submitted via RPC or P2P
relay, the complete validation pipeline runs including the new
policy.ValidatePackagePolicy check. This prevents invalid v3 transaction
chains from entering the mempool and being relayed to peers, protecting the
network from policy-violating transactions.

The integration is minimal since the heavy lifting happens in the mempool
and policy layers. The server simply constructs the mempool v2 instance with
the standard policy configuration, and the mempool handles validation
automatically during ProcessTransaction.
This commit adds end-to-end integration tests for BIP 431 (TRUC) policy
enforcement using the rpctest harness. The tests verify all six TRUC rules
plus security properties using real btcd nodes and transaction submission.

The test infrastructure enhancements to rpctest include v3 transaction
creation helpers (CreateV3Transaction, CreateV3Child) that properly handle
transaction version selection through functional options. The AddUnconfirmedTx
method on MemWallet now tracks outputs by keyIndex to enable proper UTXO
management for test scenarios involving multiple children spending different
outputs from the same parent.

The TRUC policy test suite covers:

Rule 1 (replaceability): Verifies v3 transactions signal RBF even without
BIP 125 sequence numbers, enabling reliable replacement for fee-bumping.

Rule 2 (all-or-none): Tests that v3 transactions correctly reject unconfirmed
v2 parents while accepting confirmed v2 parents, enforcing the all-or-none
TRUC requirement.

Rule 3 (topology): Validates the 1-parent-1-child constraint by testing both
valid 1P1C acceptance and rejection of long v3 chains (grandparent→parent→
child) that violate the ancestor limit.

Rules 4-5 (size limits): Confirms transactions within the size limits are
accepted. Precise size testing is deferred to unit tests where exact byte
control is easier.

Rule 6 (zero-fee): Marked as pending future package relay RPC support.

Security tests verify the topology restrictions prevent common pinning
vectors, specifically that multiple children and long chains are both
rejected.

These integration tests complement the unit tests by exercising the full
mempool acceptance pipeline with real transactions, RPC submission, and
multi-node scenarios. All tests pass, confirming the TRUC implementation is
correct and ready for production use.
@coveralls
Copy link

Pull Request Test Coverage Report for Build 18892483286

Details

  • 336 of 509 (66.01%) changed or added relevant lines in 10 files are covered.
  • 20 unchanged lines in 6 files lost coverage.
  • Overall coverage increased (+0.1%) to 56.211%

Changes Missing Coverage Covered Lines Changed/Added Lines %
mempool/txgraph/package.go 80 85 94.12%
mempool/txgraph/standard_analyzer.go 93 99 93.94%
mempool/validation_v2.go 23 30 76.67%
mempool/mempool_v2.go 10 18 55.56%
integration/rpctest/rpc_harness.go 0 9 0.0%
mempool/txgraph/graph.go 94 113 83.19%
server.go 0 20 0.0%
mempool/policy_enforcer.go 24 49 48.98%
integration/rpctest/memwallet.go 0 74 0.0%
Files with Coverage Reduction New Missed Lines %
integration/rpctest/rpc_harness.go 1 44.86%
connmgr/connmanager.go 2 84.93%
mempool/txgraph/graph.go 2 85.9%
database/ffldb/blockio.go 4 88.81%
mempool/txgraph/package.go 5 94.0%
peer/peer.go 6 70.89%
Totals Coverage Status
Change from base Build 18860192649: 0.1%
Covered Lines: 33760
Relevant Lines: 60059

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants