#11195·lnd

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:

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.