Reject new legacy channels
Author: morehouseCreated Sep 14, 2026Updated Sep 14, 2026
Legacy channels were removed from the spec in 2024, with static remote key channels replacing them as the default channel type.
LND is the only implementation that still allows new legacy channels to be created.
Impact
Legacy channels have a tweaked to_remote commitment output, which means users are unable to recover their funds after a data-loss scenario.
Reproduction
The following tests show (1) an empty channel type is accepted by LND, and (2) an empty channel type results in a legacy channel being created. Appended to funding/manager_test.go:
// TestFundingManagerAcceptEmptyChanType shows the fundee accepting an
// open_channel whose channel_type is present but empty.
func TestFundingManagerAcceptEmptyChanType(t *testing.T) {
t.Parallel()
alice, bob := setupFundingManagers(t)
t.Cleanup(func() {
tearDownFundingManagers(t, alice, bob)
})
featureBits := []lnwire.FeatureBit{
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
}
alice.localFeatures = featureBits
alice.remoteFeatures = featureBits
bob.localFeatures = featureBits
bob.remoteFeatures = featureBits
emptyChanType := (*lnwire.ChannelType)(lnwire.NewRawFeatureVector())
openChannelReq := &lnwire.OpenChannel{
ChainHash: *fundingNetParams.GenesisHash,
PendingChannelID: [32]byte{0x01},
FundingAmount: btcutil.Amount(10000000),
PushAmount: 0,
DustLimit: btcutil.Amount(546),
MaxValueInFlight: lnwire.MilliSatoshi(100000000),
ChannelReserve: btcutil.Amount(10000),
HtlcMinimum: lnwire.MilliSatoshi(1000),
FeePerKiloWeight: 15000,
CsvDelay: 144,
MaxAcceptedHTLCs: 483,
FundingKey: alice.privKey.PubKey(),
RevocationPoint: alice.privKey.PubKey(),
PaymentPoint: alice.privKey.PubKey(),
DelayedPaymentPoint: alice.privKey.PubKey(),
HtlcPoint: alice.privKey.PubKey(),
FirstCommitmentPoint: alice.privKey.PubKey(),
ChannelType: emptyChanType,
}
bob.fundingMgr.ProcessFundingMsg(openChannelReq, alice)
msg := assertFundingMsgSent(t, bob.msgChan, "AcceptChannel")
acceptChannelResponse, ok := msg.(*lnwire.AcceptChannel)
require.True(t, ok)
t.Logf("accept_channel channel_type = %v", acceptChannelResponse.ChannelType)
assertNumPendingReservations(t, bob, alicePubKey, 1)
}
// TestEmptyChanTypeOpensLegacyChannel drives the whole funding flow with an
// explicitly empty channel_type and reports the resulting channel type.
func TestEmptyChanTypeOpensLegacyChannel(t *testing.T) {
t.Parallel()
alice, bob := setupFundingManagers(t)
t.Cleanup(func() {
tearDownFundingManagers(t, alice, bob)
})
featureBits := []lnwire.FeatureBit{
lnwire.ExplicitChannelTypeOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
}
alice.localFeatures = featureBits
alice.remoteFeatures = featureBits
bob.localFeatures = featureBits
bob.remoteFeatures = featureBits
empty := (*lnwire.ChannelType)(lnwire.NewRawFeatureVector())
updateChan := make(chan *lnrpc.OpenStatusUpdate)
fundingOutPoint, fundingTx := openChannel(
t, alice, bob, btcutil.Amount(500000), 0, 1, updateChan, true,
empty,
)
chanID := lnwire.NewChanIDFromOutPoint(*fundingOutPoint)
assertErrorNotSent(t, alice.msgChan)
assertErrorNotSent(t, bob.msgChan)
sendAndCheckFirstConfirmation(t, alice, chanID, fundingTx)
sendAndCheckFirstConfirmation(t, bob, chanID, fundingTx)
assertMarkedOpen(t, alice, bob, fundingOutPoint)
channelReadyAlice, ok := assertFundingMsgSent(
t, alice.msgChan, "ChannelReady",
).(*lnwire.ChannelReady)
require.True(t, ok)
channelReadyBob, ok := assertFundingMsgSent(
t, bob.msgChan, "ChannelReady",
).(*lnwire.ChannelReady)
require.True(t, ok)
assertChannelReadySent(t, alice, bob, fundingOutPoint)
alice.fundingMgr.ProcessFundingMsg(channelReadyBob, bob)
bob.fundingMgr.ProcessFundingMsg(channelReadyAlice, alice)
report := func(node *testNode, msg *newChannelMsg) bool {
st := msg.channel.OpenChannel
t.Logf("node=%p ChanType=%v tweakless=%v anchors=%v",
node, st.ChanType, st.ChanType.IsTweakless(),
st.ChanType.HasAnchors())
require.False(t, st.ChanType.IsTweakless(),
"expected a legacy (tweaked to_remote) channel")
require.Equal(t, chanstate.SingleFunderBit, st.ChanType)
return true
}
assertHandleChannelReady(t, alice, bob, report)
}Discovery
This spec-compliance issue was discovered while fuzzing the v1 funding flow with smite.
Source: lightningnetwork/lnd