funding: fix channel type negotiation bug

The bug manifests when a nil ChannelType is passed to the funding
manager in InitFundingMsg. A default value for ChannelType is selected
and sent in the OpenChannel message. However, a nil ChannelType is
stored in the reservation context. This causes our ChannelType checks in
handleFundingAccept to be bypassed.

Usually this makes us end up in the "peer unexpectedly sent explicit
ChannelType" case, where we can still recover by reselecting a default
ChannelType and verifying it matches the one the peer sent. But if the
peer sends a nil ChannelType, we miss it.

While fixing the bug, I also tried to simplify the negotiation logic, as
the complexity is likely what hid the bug in the first place.

Now negotiateCommitmentType only returns the ChannelType to be used in
OpenChannel/AcceptChannel and the CommitmentType to pass to the wallet.
It will even return a nil ChannelType when we're supposed to use implicit
negotiation, so we don't need to manually set it to nil for OpenChannel
and AcceptChannel.
This commit is contained in:
Matt Morehouse 2023-01-16 14:18:54 -06:00
parent 0ae9c63d64
commit a8a50f32f5
No known key found for this signature in database
GPG key ID: CC8ECA224831C982
4 changed files with 130 additions and 105 deletions

View file

@ -8,12 +8,6 @@ import (
) )
var ( 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 // errUnsupportedCommitmentType is an error returned when a specific
// channel commitment type is being explicitly negotiated but either // channel commitment type is being explicitly negotiated but either
// peer of the channel does not support it. // peer of the channel does not support it.
@ -24,38 +18,47 @@ var (
// negotiateCommitmentType negotiates the commitment type of a newly opened // negotiateCommitmentType negotiates the commitment type of a newly opened
// channel. If a desiredChanType is provided, explicit negotiation for said type // channel. If a desiredChanType is provided, explicit negotiation for said type
// will be attempted if the set of both local and remote features support it. // will be attempted if the set of both local and remote features support it.
// Otherwise, implicit negotiation will be attempted. Two booleans are // Otherwise, implicit negotiation will be attempted.
// returned letting the caller know if the option-scid-alias or zero-conf //
// channel types were negotiated. // The returned ChannelType is nil when implicit negotiation is used. An error
// is only returned if desiredChanType is not supported.
func negotiateCommitmentType(desiredChanType *lnwire.ChannelType, local, func negotiateCommitmentType(desiredChanType *lnwire.ChannelType, local,
remote *lnwire.FeatureVector, mustBeExplicit bool) (bool, remote *lnwire.FeatureVector) (*lnwire.ChannelType,
*lnwire.ChannelType, lnwallet.CommitmentType, error) { lnwallet.CommitmentType, error) {
if desiredChanType != nil {
// If the peer does know explicit negotiation, let's attempt
// that now.
if hasFeatures(
local, remote, lnwire.ExplicitChannelTypeOptional,
) {
// BOLT#2 specifies we MUST use explicit negotiation if both peers
// signal for it.
if hasFeatures(local, remote, lnwire.ExplicitChannelTypeOptional) {
if desiredChanType != nil {
commitType, err := explicitNegotiateCommitmentType( commitType, err := explicitNegotiateCommitmentType(
*desiredChanType, local, remote, *desiredChanType, local, remote,
) )
return true, desiredChanType, commitType, err
return desiredChanType, commitType, err
} }
// If we're the funder, and we are attempting to use an // Explicitly signal the "implicit" negotiation commitment type
// explicit channel type, but the remote party doesn't signal // as default when a desired channel type isn't specified.
// the bit, then we actually want to exit here, to ensure the chanType, commitType := implicitNegotiateCommitmentType(local,
// user doesn't end up with an unexpected channel type via remote)
// implicit negotiation.
if mustBeExplicit { return chanType, commitType, nil
return false, nil, 0, errUnsupportedExplicitNegotiation }
// Otherwise, we'll use implicit negotiation. In this case, we are
// restricted to the newest channel type advertised. If the passed-in
// channelType doesn't match what was advertised, we fail.
chanType, commitType := implicitNegotiateCommitmentType(local, remote)
if desiredChanType != nil {
expected := lnwire.RawFeatureVector(*desiredChanType)
actual := lnwire.RawFeatureVector(*chanType)
if !expected.Equals(&actual) {
return nil, 0, errUnsupportedChannelType
} }
} }
chanType, commitType := implicitNegotiateCommitmentType(local, remote) return nil, commitType, nil
return false, chanType, commitType, nil
} }
// explicitNegotiateCommitmentType attempts to explicitly negotiate for a // explicitNegotiateCommitmentType attempts to explicitly negotiate for a

View file

@ -15,7 +15,6 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
mustBeExplicit bool
channelFeatures *lnwire.RawFeatureVector channelFeatures *lnwire.RawFeatureVector
localFeatures *lnwire.RawFeatureVector localFeatures *lnwire.RawFeatureVector
remoteFeatures *lnwire.RawFeatureVector remoteFeatures *lnwire.RawFeatureVector
@ -41,32 +40,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
lnwire.AnchorsZeroFeeHtlcTxOptional, lnwire.AnchorsZeroFeeHtlcTxOptional,
), ),
expectsCommitType: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, expectsCommitType: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx,
expectsChanType: (*lnwire.ChannelType)( expectsChanType: nil,
lnwire.NewRawFeatureVector( expectsErr: nil,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
),
),
expectsErr: nil,
},
{
name: "local funder wants explicit, remote doesn't " +
"support so fall back",
mustBeExplicit: true,
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", name: "explicit missing remote commitment feature",
@ -282,9 +257,9 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
}, },
// Both sides signal the explicit chan type bit, so we expect // Both sides signal the explicit chan type bit, so we expect
// that we return the corresponding chan type feature bits, // that we return the corresponding chan type feature bits,
// even though we didn't set an explicit channel type. // even though we didn't set a desired channel type.
{ {
name: "implicit anchors", name: "default explicit anchors",
channelFeatures: nil, channelFeatures: nil,
localFeatures: lnwire.NewRawFeatureVector( localFeatures: lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyOptional, lnwire.StaticRemoteKeyOptional,
@ -316,12 +291,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
lnwire.StaticRemoteKeyOptional, lnwire.StaticRemoteKeyOptional,
), ),
expectsCommitType: lnwallet.CommitmentTypeTweakless, expectsCommitType: lnwallet.CommitmentTypeTweakless,
expectsChanType: (*lnwire.ChannelType)( expectsChanType: nil,
lnwire.NewRawFeatureVector( expectsErr: nil,
lnwire.StaticRemoteKeyRequired,
),
),
expectsErr: nil,
}, },
{ {
name: "implicit legacy", name: "implicit legacy",
@ -332,10 +303,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
lnwire.AnchorsZeroFeeHtlcTxOptional, lnwire.AnchorsZeroFeeHtlcTxOptional,
), ),
expectsCommitType: lnwallet.CommitmentTypeLegacy, expectsCommitType: lnwallet.CommitmentTypeLegacy,
expectsChanType: (*lnwire.ChannelType)( expectsChanType: nil,
lnwire.NewRawFeatureVector(), expectsErr: nil,
),
expectsErr: nil,
}, },
} }
@ -357,9 +326,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
) )
} }
_, lChan, lCommit, err := negotiateCommitmentType( lChan, lCommit, err := negotiateCommitmentType(
channelType, localFeatures, remoteFeatures, channelType, localFeatures, remoteFeatures,
testCase.mustBeExplicit,
) )
var ( var (
@ -383,9 +351,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
require.Equal(t, testCase.scidAlias, localScid) require.Equal(t, testCase.scidAlias, localScid)
require.Equal(t, testCase.expectsErr, err) require.Equal(t, testCase.expectsErr, err)
_, rChan, rCommit, err := negotiateCommitmentType( rChan, rCommit, err := negotiateCommitmentType(
channelType, remoteFeatures, localFeatures, channelType, remoteFeatures, localFeatures,
testCase.mustBeExplicit,
) )
if rChan != nil { if rChan != nil {

View file

@ -1452,9 +1452,8 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
// the remote peer are signaling the proper feature bit if we're using // the remote peer are signaling the proper feature bit if we're using
// implicit negotiation, and simply the channel type sent over if we're // implicit negotiation, and simply the channel type sent over if we're
// using explicit negotiation. // using explicit negotiation.
wasExplicit, _, commitType, err := negotiateCommitmentType( chanType, commitType, err := negotiateCommitmentType(
msg.ChannelType, peer.LocalFeatures(), peer.RemoteFeatures(), msg.ChannelType, peer.LocalFeatures(), peer.RemoteFeatures(),
false,
) )
if err != nil { if err != nil {
// TODO(roasbeef): should be using soft errors // TODO(roasbeef): should be using soft errors
@ -1473,19 +1472,16 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
} }
var ( var (
chanTypeFeatureBits *lnwire.ChannelType zeroConf bool
zeroConf bool scid bool
scid bool
) )
if wasExplicit { // Only echo back a channel type in AcceptChannel if we actually used
// Only echo back a channel type in AcceptChannel if we actually // explicit negotiation above.
// used explicit negotiation above. if chanType != nil {
chanTypeFeatureBits = msg.ChannelType
// Check if the channel type includes the zero-conf or // Check if the channel type includes the zero-conf or
// scid-alias bits. // scid-alias bits.
featureVec := lnwire.RawFeatureVector(*chanTypeFeatureBits) featureVec := lnwire.RawFeatureVector(*chanType)
zeroConf = featureVec.IsSet(lnwire.ZeroConfRequired) zeroConf = featureVec.IsSet(lnwire.ZeroConfRequired)
scid = featureVec.IsSet(lnwire.ScidAliasRequired) scid = featureVec.IsSet(lnwire.ScidAliasRequired)
@ -1727,7 +1723,7 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
remoteMaxHtlcs: maxHtlcs, remoteMaxHtlcs: maxHtlcs,
remoteChanReserve: chanReserve, remoteChanReserve: chanReserve,
maxLocalCsv: f.cfg.MaxLocalCSVDelay, maxLocalCsv: f.cfg.MaxLocalCSVDelay,
channelType: msg.ChannelType, channelType: chanType,
err: make(chan error, 1), err: make(chan error, 1),
peer: peer, peer: peer,
} }
@ -1800,7 +1796,7 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey, HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint, FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
UpfrontShutdownScript: ourContribution.UpfrontShutdown, UpfrontShutdownScript: ourContribution.UpfrontShutdown,
ChannelType: chanTypeFeatureBits, ChannelType: chanType,
LeaseExpiry: msg.LeaseExpiry, LeaseExpiry: msg.LeaseExpiry,
} }
@ -1888,12 +1884,9 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer,
peer.LocalFeatures(), peer.RemoteFeatures(), peer.LocalFeatures(), peer.RemoteFeatures(),
) )
// We pass in false here as the funder since at this point, we _, negotiatedCommitType, err := negotiateCommitmentType(
// didn't set a chan type ourselves, so falling back to
// implicit funding is acceptable.
_, _, negotiatedCommitType, err := negotiateCommitmentType(
msg.ChannelType, peer.LocalFeatures(), msg.ChannelType, peer.LocalFeatures(),
peer.RemoteFeatures(), false, peer.RemoteFeatures(),
) )
if err != nil { if err != nil {
err := errors.New("received unexpected channel type") err := errors.New("received unexpected channel type")
@ -4055,9 +4048,9 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
// Before we init the channel, we'll also check to see what commitment // Before we init the channel, we'll also check to see what commitment
// format we can use with this peer. This is dependent on *both* us and // format we can use with this peer. This is dependent on *both* us and
// the remote peer are signaling the proper feature bit. // the remote peer are signaling the proper feature bit.
_, chanType, commitType, err := negotiateCommitmentType( chanType, commitType, err := negotiateCommitmentType(
msg.ChannelType, msg.Peer.LocalFeatures(), msg.ChannelType, msg.Peer.LocalFeatures(),
msg.Peer.RemoteFeatures(), true, msg.Peer.RemoteFeatures(),
) )
if err != nil { if err != nil {
log.Errorf("channel type negotiation failed: %v", err) log.Errorf("channel type negotiation failed: %v", err)
@ -4070,20 +4063,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
scid bool scid bool
) )
// Check if the returned chanType includes either the zero-conf or if chanType != nil {
// scid-alias bits. // Check if the returned chanType includes either the zero-conf
featureVec := lnwire.RawFeatureVector(*chanType) // or scid-alias bits.
zeroConf = featureVec.IsSet(lnwire.ZeroConfRequired) featureVec := lnwire.RawFeatureVector(*chanType)
scid = featureVec.IsSet(lnwire.ScidAliasRequired) zeroConf = featureVec.IsSet(lnwire.ZeroConfRequired)
scid = featureVec.IsSet(lnwire.ScidAliasRequired)
// The option-scid-alias channel type for a public channel is // The option-scid-alias channel type for a public channel is
// disallowed. // disallowed.
if scid && !msg.Private { if scid && !msg.Private {
err = fmt.Errorf("option-scid-alias chantype for public " + err = fmt.Errorf("option-scid-alias chantype for " +
"channel") "public channel")
log.Error(err) log.Error(err)
msg.Err <- err msg.Err <- err
return
return
}
} }
// First, we'll query the fee estimator for a fee that should get the // First, we'll query the fee estimator for a fee that should get the
@ -4239,7 +4235,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
remoteMaxHtlcs: maxHtlcs, remoteMaxHtlcs: maxHtlcs,
remoteChanReserve: chanReserve, remoteChanReserve: chanReserve,
maxLocalCsv: maxCSV, maxLocalCsv: maxCSV,
channelType: msg.ChannelType, channelType: chanType,
reservation: reservation, reservation: reservation,
peer: msg.Peer, peer: msg.Peer,
updates: msg.Updates, updates: msg.Updates,

View file

@ -4493,3 +4493,62 @@ func TestCommitmentTypeFundmaxSanityCheck(t *testing.T) {
} }
} }
} }
// TestFundingManagerNoEchoChanType tests that the funding flow is aborted if
// the peer fails to echo back the channel type in AcceptChannel.
func TestFundingManagerNoEchoChanType(t *testing.T) {
t.Parallel()
alice, bob := setupFundingManagers(t)
t.Cleanup(func() {
tearDownFundingManagers(t, alice, bob)
})
// Alice and Bob will have the same set of feature bits in our test.
featureBits := []lnwire.FeatureBit{
lnwire.ExplicitChannelTypeOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
}
alice.localFeatures = featureBits
alice.remoteFeatures = featureBits
bob.localFeatures = featureBits
bob.remoteFeatures = featureBits
expectedChanType := (*lnwire.ChannelType)(lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
))
// Create a funding request and start the workflow.
updateChan := make(chan *lnrpc.OpenStatusUpdate)
errChan := make(chan error, 1)
initReq := &InitFundingMsg{
Peer: bob,
TargetPubkey: bob.privKey.PubKey(),
ChainHash: *fundingNetParams.GenesisHash,
LocalFundingAmt: 500000,
Updates: updateChan,
Err: errChan,
}
alice.fundingMgr.InitFundingWorkflow(initReq)
// Alice should have sent the OpenChannel message to Bob.
openChanMsg := expectOpenChannelMsg(t, alice.msgChan)
require.Equal(t, expectedChanType, openChanMsg.ChannelType)
// Let Bob handle the OpenChannel message.
bob.fundingMgr.ProcessFundingMsg(openChanMsg, alice)
acceptChanMsg, _ := assertFundingMsgSent(t, bob.msgChan,
"AcceptChannel").(*lnwire.AcceptChannel)
require.Equal(t, expectedChanType, acceptChanMsg.ChannelType)
// Drop the channel type and ensure Alice responds with an error.
acceptChanMsg.ChannelType = nil
alice.fundingMgr.ProcessFundingMsg(acceptChanMsg, bob)
assertFundingMsgSent(t, alice.msgChan, "Error")
}