Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
da9a815
deps: update btcd, btcwallet, and neutrino for P2A support
Roasbeef Jan 6, 2026
6382afe
lnwire: add feature bits 40/41 for zero-fee commitments
Roasbeef Jan 6, 2026
7ea3098
channeldb: add ZeroFeeCommitmentsBit channel type
Roasbeef Jan 6, 2026
258ba8c
lnwallet: add CommitmentTypeZeroFee and update constraints API
Roasbeef Jan 6, 2026
8956747
lncfg: add --protocol.zero-fee-commitments config flag
Roasbeef Jan 6, 2026
6343651
feature: add NoZeroFeeCommitments config option to manager
Roasbeef Jan 6, 2026
dd0548a
feature: add ZeroFeeCommitmentsOptional to default feature sets
Roasbeef Jan 6, 2026
6aedf93
server: wire up NoZeroFeeCommitments to feature manager
Roasbeef Jan 6, 2026
e0a526c
input: add ZeroFeeCommitWeight and MaxHTLCNumberV3 constants
Roasbeef Jan 6, 2026
bdcab40
input: add ZeroFeeAnchorSpend witness type for P2A outputs
Roasbeef Jan 6, 2026
cc1adcb
lnwallet: implement v3 commitment transaction building
Roasbeef Jan 6, 2026
5218c68
lnwallet: use version 3 for HTLC success and timeout transactions
Roasbeef Jan 6, 2026
de84408
lnwallet: reject update_fee for zero-fee commitment channels
Roasbeef Jan 6, 2026
e36f43f
htlcswitch: add ErrUpdateFeeNotAllowed failure code
Roasbeef Jan 6, 2026
4e9bca5
htlcswitch: handle update_fee for zero-fee commitment channels
Roasbeef Jan 6, 2026
63d4907
funding: add zero-fee commitment channel type negotiation
Roasbeef Jan 6, 2026
df74a3f
funding: validate zero feerate and pass commit type to constraints
Roasbeef Jan 6, 2026
f128c18
lnwallet: pass commitment type to VerifyConstraints in wallet
Roasbeef Jan 6, 2026
920505e
contractcourt: use ZeroFeeAnchorSpend witness type for P2A anchors
Roasbeef Jan 6, 2026
122a88b
contractcourt: fix anchor sweeping for P2A outputs
Roasbeef Jan 6, 2026
6cd483e
lnrpc: add ZERO_FEE to CommitmentType enum
Roasbeef Jan 6, 2026
95f9638
lnrpc: regenerate protobuf files for ZERO_FEE enum
Roasbeef Jan 6, 2026
7f4b9d5
rpcserver: map ZeroFeeCommitmentsBit to ZERO_FEE enum
Roasbeef Jan 6, 2026
c302923
rpcserver: add test case for zero fee commitment type mapping
Roasbeef Jan 6, 2026
78d8ca0
multi: adapt to neutrino Start(context.Context) API change
Roasbeef Jan 6, 2026
e344bf3
lnwallet: update CreateCommitTx call with trimmed amount parameter
Roasbeef Jan 6, 2026
4db5e6c
lnwallet: add spec test vectors for v3 zero-fee commitments
Roasbeef Jan 6, 2026
33bb56f
input: add tests for ZeroFeeAnchorSpend witness type
Roasbeef Jan 6, 2026
4a59f8a
contractcourt: add tests for P2A anchor resolver
Roasbeef Jan 6, 2026
dab2274
lnwallet: add test for update_fee rejection on zero-fee channels
Roasbeef Jan 6, 2026
a7ddf1b
lnwallet: add tests for zero-fee anchor amount calculation
Roasbeef Jan 6, 2026
793c63c
lnwallet: add test for max HTLC validation with zero-fee channels
Roasbeef Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ const (
// level tapscript commitment. This MUST be set along with the
// SimpleTaprootFeatureBit.
TapscriptRootBit ChannelType = 1 << 11

// ZeroFeeCommitmentsBit indicates that the channel uses v3 transactions
// with zero-fee commitment and HTLC transactions. Fee bumping is done
// via CPFP on the P2A (pay-to-anchor) output.
ZeroFeeCommitmentsBit ChannelType = 1 << 12
)

// IsSingleFunder returns true if the channel type if one of the known single
Expand Down Expand Up @@ -496,6 +501,12 @@ func (c ChannelType) HasTapscriptRoot() bool {
return c&TapscriptRootBit == TapscriptRootBit
}

// HasZeroFeeCommitments returns true if the channel uses v3 transactions with
// zero-fee commitment and HTLC transactions.
func (c ChannelType) HasZeroFeeCommitments() bool {
return c&ZeroFeeCommitmentsBit == ZeroFeeCommitmentsBit
}

// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
// that are responsible for providing bounds on the state space of the abstract
// channel state. These values must be remembered for normal channel operation
Expand Down
2 changes: 1 addition & 1 deletion config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/btcsuite/btcwallet/chain"

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 22 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist
"github.com/btcsuite/btcwallet/waddrmgr"

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 23 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist
"github.com/btcsuite/btcwallet/wallet"

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 24 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist
"github.com/btcsuite/btcwallet/walletdb"

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist

Check failure on line 25 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/btcsuite/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/btcsuite/btcwallet does not exist
proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/blockntfns"

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 28 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist
"github.com/lightninglabs/neutrino/headerfs"

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race)

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Check commits

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="kvdb_etcd")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_sqlite")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-cover)

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit-race tags="test_db_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist

Check failure on line 29 in config_builder.go

View workflow job for this annotation

GitHub Actions / Run unit tests (unit tags="test_db_postgres")

github.com/lightninglabs/[email protected]: replacement directory /Users/roasbeef/gocode/src/github.com/lightninglabs/neutrino does not exist
"github.com/lightninglabs/neutrino/pushtx"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"
Expand Down Expand Up @@ -1739,7 +1739,7 @@
"client: %v", err)
}

if err := neutrinoCS.Start(); err != nil {
if err := neutrinoCS.Start(context.Background()); err != nil {
db.Close()
return nil, nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions contractcourt/anchor_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ func (c *anchorResolver) Launch() error {
witnessType = input.TaprootAnchorSweepSpend
}

// For zero-fee commitment channels, the anchor is a P2A output that
// can be spent with an empty witness.
if c.chanType.HasZeroFeeCommitments() {
witnessType = input.ZeroFeeAnchorSpend
}

anchorInput := input.MakeBaseInput(
&c.anchor, witnessType, &c.anchorSignDescriptor,
c.broadcastHeight, nil,
Expand Down
232 changes: 232 additions & 0 deletions contractcourt/anchor_resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package contractcourt

import (
"testing"

"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/stretchr/testify/require"
)

// TestAnchorResolverWitnessType verifies that the anchor resolver selects the
// correct witness type based on the channel type.
func TestAnchorResolverWitnessType(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
chanType channeldb.ChannelType
expectedWitnessType input.StandardWitnessType
}{
{
name: "legacy anchor channel",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit,
expectedWitnessType: input.CommitmentAnchor,
},
{
name: "zero fee htlc tx anchor channel",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit,
expectedWitnessType: input.CommitmentAnchor,
},
{
name: "zero fee commitment channel (v3)",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.ZeroFeeCommitmentsBit,
expectedWitnessType: input.ZeroFeeAnchorSpend,
},
{
name: "taproot channel",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SimpleTaprootFeatureBit,
expectedWitnessType: input.TaprootAnchorSweepSpend,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

// Verify the channel type methods return expected values.
switch tc.expectedWitnessType {
case input.ZeroFeeAnchorSpend:
require.True(
t, tc.chanType.HasZeroFeeCommitments(),
"should have zero fee commitments",
)
case input.TaprootAnchorSweepSpend:
require.True(
t, tc.chanType.IsTaproot(),
"should be taproot",
)
case input.CommitmentAnchor:
require.False(
t, tc.chanType.HasZeroFeeCommitments(),
"should not have zero fee commitments",
)
require.False(
t, tc.chanType.IsTaproot(),
"should not be taproot",
)
}

// Test the witness type selection logic directly.
// This mirrors the logic in anchor_resolver.go Launch().
witnessType := input.CommitmentAnchor
if tc.chanType.IsTaproot() {
witnessType = input.TaprootAnchorSweepSpend
}
if tc.chanType.HasZeroFeeCommitments() {
witnessType = input.ZeroFeeAnchorSpend
}

require.Equal(
t, tc.expectedWitnessType, witnessType,
"witness type mismatch",
)
})
}
}

// TestAnchorResolverZeroFeeWitnessSize verifies that the ZeroFeeAnchorSpend
// witness type returns the correct size upper bound.
func TestAnchorResolverZeroFeeWitnessSize(t *testing.T) {
t.Parallel()

// ZeroFeeAnchorSpend should return size 1 (empty witness = 1 byte
// for the element count of 0).
size, isNestedP2SH, err := input.ZeroFeeAnchorSpend.SizeUpperBound()
require.NoError(t, err)
require.False(t, isNestedP2SH)
require.EqualValues(t, 1, size, "P2A witness size should be 1 byte")

// Compare with regular anchor sizes.
regularSize, _, err := input.CommitmentAnchor.SizeUpperBound()
require.NoError(t, err)
require.Greater(
t, regularSize, size,
"regular anchor witness should be larger than P2A",
)
}

// TestAnchorResolverChannelTypePriority verifies that zero-fee commitment
// check takes precedence over taproot check (if a channel were both, which
// shouldn't happen in practice).
func TestAnchorResolverChannelTypePriority(t *testing.T) {
t.Parallel()

// The logic in anchor_resolver.go checks taproot first, then
// zero-fee commitments. This means zero-fee commitments takes
// precedence. Test that the ordering is correct.

// A hypothetical channel type with both flags (shouldn't exist in
// practice but tests the code path ordering).
hypotheticalType := channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SimpleTaprootFeatureBit |
channeldb.ZeroFeeCommitmentsBit

// Verify that both checks would return true.
require.True(t, hypotheticalType.IsTaproot())
require.True(t, hypotheticalType.HasZeroFeeCommitments())

// Apply the same logic as anchor_resolver.go.
witnessType := input.CommitmentAnchor
if hypotheticalType.IsTaproot() {
witnessType = input.TaprootAnchorSweepSpend
}
if hypotheticalType.HasZeroFeeCommitments() {
witnessType = input.ZeroFeeAnchorSpend
}

// Zero-fee commitments should win because it's checked after taproot.
require.Equal(
t, input.ZeroFeeAnchorSpend, witnessType,
"zero-fee commitments should take precedence over taproot",
)
}

// TestZeroFeeAnchorOutputValue verifies the expected anchor output value for
// v3 zero-fee commitment channels.
func TestZeroFeeAnchorOutputValue(t *testing.T) {
t.Parallel()

// P2A anchor max amount is 240 sats (per spec).
const p2aAnchorMaxAmount = 240

// The P2A output script is 4 bytes: OP_1 (0x51) + 2 (0x02) + 0x4e73.
p2aScriptLen := 4

// Verify the anchor output is economically viable to spend.
// At 1 sat/vB, a P2A spend is:
// - Input: 41 vB (outpoint + sequence + empty witness)
// - P2A witness: 1 vB (just the witness count of 0)
// Total: ~42 vB = 42 sats at 1 sat/vB
//
// So even a 0-value anchor can be economically swept at low fee rates
// when used for CPFP.
require.LessOrEqual(
t, p2aScriptLen, 4,
"P2A script should be 4 bytes",
)

// Anchor amount can range from 0 to 240 sats.
require.LessOrEqual(
t, int64(0), int64(p2aAnchorMaxAmount),
"anchor amount should be within valid range",
)
}

// TestAnchorResolverSupplementState verifies that the anchor resolver correctly
// receives channel type information via SupplementState.
func TestAnchorResolverSupplementState(t *testing.T) {
t.Parallel()

// Create an anchor resolver with minimal config.
desc := input.SignDescriptor{
Output: &wire.TxOut{
Value: 240,
PkScript: []byte{0x51, 0x02, 0x4e, 0x73}, // P2A script
},
}

resolver := &anchorResolver{
anchorSignDescriptor: desc,
}

// Before SupplementState, chanType should be zero.
require.Equal(
t, channeldb.ChannelType(0), resolver.chanType,
"chanType should be zero before SupplementState",
)

// Create a mock channel state with zero-fee commitments.
chanState := &channeldb.OpenChannel{
ChanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.ZeroFeeCommitmentsBit,
}

// Call SupplementState.
resolver.SupplementState(chanState)

// Verify chanType was set correctly.
require.Equal(
t, chanState.ChanType, resolver.chanType,
"chanType should match after SupplementState",
)
require.True(
t, resolver.chanType.HasZeroFeeCommitments(),
"resolver should recognize zero-fee commitments",
)
}
19 changes: 15 additions & 4 deletions contractcourt/channel_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3167,10 +3167,17 @@ func (c *ChannelArbitrator) createSweepRequest(
if txscript.IsPayToTaproot(
anchor.AnchorSignDescriptor.Output.PkScript,
) {

witnessType = input.TaprootAnchorSweepSpend
}

// For zero-fee commitment channels, the anchor is a P2A output that
// can be spent with an empty witness.
if txscript.IsPayToAnchorScript(
anchor.AnchorSignDescriptor.Output.PkScript,
) {
witnessType = input.ZeroFeeAnchorSpend
}

// Prepare anchor output for sweeping.
anchorInput := input.MakeBaseInput(
&anchor.CommitAnchor,
Expand All @@ -3194,11 +3201,15 @@ func (c *ChannelArbitrator) createSweepRequest(

// Calculate the budget based on the value under protection, which is
// the sum of all HTLCs on this commitment subtracted by their budgets.
// The anchor output in itself has a small output value of 330 sats so
// we also include it in the budget to pay for the cpfp transaction.
// The anchor output value is also included in the budget to pay for
// the cpfp transaction. For legacy anchors this is 330 sats, for
// zero-fee P2A anchors it's typically 240 sats or less.
anchorValue := btcutil.Amount(
anchor.AnchorSignDescriptor.Output.Value,
)
budget := calculateBudget(
value, c.cfg.Budget.AnchorCPFPRatio, c.cfg.Budget.AnchorCPFP,
) + AnchorOutputValue
) + anchorValue

log.Infof("ChannelArbitrator(%v): offering anchor from %s commitment "+
"%v to sweeper with deadline=%v, budget=%v", c.cfg.ChanPoint,
Expand Down
4 changes: 4 additions & 0 deletions feature/default_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.ZeroFeeCommitmentsOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
}
9 changes: 9 additions & 0 deletions feature/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ type Config struct {
// coop close.
NoRbfCoopClose bool

// NoZeroFeeCommitments unsets any bits signaling support for v3
// zero-fee commitment channels with P2A anchor outputs. This is the
// default since zero-fee commitments must be explicitly enabled.
NoZeroFeeCommitments bool

// CustomFeatures is a set of custom features to advertise in each
// set.
CustomFeatures map[Set][]lnwire.FeatureBit
Expand Down Expand Up @@ -221,6 +226,10 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
raw.Unset(lnwire.RbfCoopCloseOptionalStaging)
raw.Unset(lnwire.RbfCoopCloseOptional)
}
if cfg.NoZeroFeeCommitments {
raw.Unset(lnwire.ZeroFeeCommitmentsOptional)
raw.Unset(lnwire.ZeroFeeCommitmentsRequired)
}

for _, custom := range cfg.CustomFeatures[set] {
if custom > set.Maximum() {
Expand Down
Loading
Loading