From e7885f2bde37e9d2ab8c496fc66b9e910e7e5d49 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 9 Jun 2021 12:43:39 -0700 Subject: [PATCH] funding: add explicit commitment type negotiation support This commit adds the ability for a channel initiator/responder to determine whether the channel to be opened can use a specific commitment type through explicit negotiation. It also includes the existing implicit negotiation logic to fall back on if explicit negotiation is not supported. --- funding/commitment_type_negotiation.go | 120 ++++++++++++ funding/commitment_type_negotiation_test.go | 191 ++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 funding/commitment_type_negotiation.go create mode 100644 funding/commitment_type_negotiation_test.go diff --git a/funding/commitment_type_negotiation.go b/funding/commitment_type_negotiation.go new file mode 100644 index 000000000..e66dc78b2 --- /dev/null +++ b/funding/commitment_type_negotiation.go @@ -0,0 +1,120 @@ +package funding + +import ( + "errors" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // errUnsupportedExplicitNegotiation is an error returned when explicit + // channel commitment negotiation is attempted but either peer of the + // channel does not support it. + errUnsupportedExplicitNegotiation = errors.New("explicit channel " + + "type negotiation not supported") + + // errUnsupportedCommitmentType is an error returned when a specific + // channel commitment type is being explicitly negotiated but either + // peer of the channel does not support it. + errUnsupportedChannelType = errors.New("requested channel type " + + "not supported") +) + +// negotiateCommitmentType negotiates the commitment type of a newly opened +// channel. If a channelType is provided, explicit negotiation for said type +// will be attempted if the set of both local and remote features support it. +// Otherwise, implicit negotiation will be attempted. +func negotiateCommitmentType(channelType *lnwire.ChannelType, + local, remote *lnwire.FeatureVector) (lnwallet.CommitmentType, error) { + + if channelType != nil { + if !hasFeatures(local, remote, lnwire.ExplicitChannelTypeOptional) { + return 0, errUnsupportedExplicitNegotiation + } + return explicitNegotiateCommitmentType( + *channelType, local, remote, + ) + } + + return implicitNegotiateCommitmentType(local, remote), nil +} + +// explicitNegotiateCommitmentType attempts to explicitly negotiate for a +// specific channel type. Since the channel type is comprised of a set of even +// feature bits, we also make sure each feature is supported by both peers. An +// error is returned if either peer does not support said channel type. +func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, + local, remote *lnwire.FeatureVector) (lnwallet.CommitmentType, error) { + + channelFeatures := lnwire.RawFeatureVector(channelType) + + switch { + // Anchors zero fee + static remote key features only. + case channelFeatures.OnlyContains( + lnwire.AnchorsZeroFeeHtlcTxRequired, + lnwire.StaticRemoteKeyRequired, + ): + if !hasFeatures( + local, remote, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.StaticRemoteKeyOptional, + ) { + return 0, errUnsupportedChannelType + } + return lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, nil + + // Static remote key feature only. + case channelFeatures.OnlyContains(lnwire.StaticRemoteKeyRequired): + if !hasFeatures(local, remote, lnwire.StaticRemoteKeyOptional) { + return 0, errUnsupportedChannelType + } + return lnwallet.CommitmentTypeTweakless, nil + + // No features, use legacy commitment type. + case channelFeatures.IsEmpty(): + return lnwallet.CommitmentTypeLegacy, nil + + default: + return 0, errUnsupportedChannelType + } +} + +// implicitNegotiateCommitmentType negotiates the commitment type of a channel +// implicitly by choosing the latest type supported by the local and remote +// fetures. +func implicitNegotiateCommitmentType(local, + remote *lnwire.FeatureVector) lnwallet.CommitmentType { + + // If both peers are signalling support for anchor commitments with + // zero-fee HTLC transactions, we'll use this type. + if hasFeatures(local, remote, lnwire.AnchorsZeroFeeHtlcTxOptional) { + return lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx + } + + // Since we don't want to support the "legacy" anchor type, we will fall + // back to static remote key if the nodes don't support the zero fee + // HTLC tx anchor type. + // + // If both nodes are signaling the proper feature bit for tweakless + // commitments, we'll use that. + if hasFeatures(local, remote, lnwire.StaticRemoteKeyOptional) { + return lnwallet.CommitmentTypeTweakless + } + + // Otherwise we'll fall back to the legacy type. + return lnwallet.CommitmentTypeLegacy +} + +// hasFeatures determines whether a set of features is supported by both the set +// of local and remote features. +func hasFeatures(local, remote *lnwire.FeatureVector, + features ...lnwire.FeatureBit) bool { + + for _, feature := range features { + if !local.HasFeature(feature) || !remote.HasFeature(feature) { + return false + } + } + return true +} diff --git a/funding/commitment_type_negotiation_test.go b/funding/commitment_type_negotiation_test.go new file mode 100644 index 000000000..c9ac25a7a --- /dev/null +++ b/funding/commitment_type_negotiation_test.go @@ -0,0 +1,191 @@ +package funding + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestCommitmentTypeNegotiation tests all of the possible paths of a channel +// commitment type negotiation. +func TestCommitmentTypeNegotiation(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + channelFeatures *lnwire.RawFeatureVector + localFeatures *lnwire.RawFeatureVector + remoteFeatures *lnwire.RawFeatureVector + expectsRes lnwallet.CommitmentType + expectsErr error + }{ + { + name: "explicit missing remote negotiation feature", + channelFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyRequired, + lnwire.AnchorsZeroFeeHtlcTxRequired, + ), + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + ), + expectsErr: errUnsupportedExplicitNegotiation, + }, + { + name: "explicit missing remote commitment feature", + channelFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyRequired, + lnwire.AnchorsZeroFeeHtlcTxRequired, + ), + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.ExplicitChannelTypeOptional, + ), + expectsErr: errUnsupportedChannelType, + }, + { + name: "explicit anchors", + channelFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyRequired, + lnwire.AnchorsZeroFeeHtlcTxRequired, + ), + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + expectsRes: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, + expectsErr: nil, + }, + { + name: "explicit tweakless", + channelFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyRequired, + ), + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + expectsRes: lnwallet.CommitmentTypeTweakless, + expectsErr: nil, + }, + { + name: "explicit legacy", + channelFeatures: lnwire.NewRawFeatureVector(), + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + expectsRes: lnwallet.CommitmentTypeLegacy, + expectsErr: nil, + }, + { + name: "implicit anchors", + channelFeatures: nil, + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.ExplicitChannelTypeOptional, + ), + expectsRes: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, + expectsErr: nil, + }, + { + name: "implicit tweakless", + channelFeatures: nil, + localFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + ), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + ), + expectsRes: lnwallet.CommitmentTypeTweakless, + expectsErr: nil, + }, + { + name: "implicit legacy", + channelFeatures: nil, + localFeatures: lnwire.NewRawFeatureVector(), + remoteFeatures: lnwire.NewRawFeatureVector( + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + ), + expectsRes: lnwallet.CommitmentTypeLegacy, + expectsErr: nil, + }, + } + + for _, testCase := range testCases { + testCase := testCase + ok := t.Run(testCase.name, func(t *testing.T) { + localFeatures := lnwire.NewFeatureVector( + testCase.localFeatures, lnwire.Features, + ) + remoteFeatures := lnwire.NewFeatureVector( + testCase.remoteFeatures, lnwire.Features, + ) + + var channelType *lnwire.ChannelType + if testCase.channelFeatures != nil { + channelType = new(lnwire.ChannelType) + *channelType = lnwire.ChannelType( + *testCase.channelFeatures, + ) + } + localType, err := negotiateCommitmentType( + channelType, localFeatures, remoteFeatures, + ) + require.Equal(t, testCase.expectsErr, err) + + remoteType, err := negotiateCommitmentType( + channelType, remoteFeatures, localFeatures, + ) + require.Equal(t, testCase.expectsErr, err) + + if testCase.expectsErr != nil { + return + } + + require.Equal(t, testCase.expectsRes, localType) + require.Equal(t, testCase.expectsRes, remoteType) + }) + if !ok { + return + } + } +}