From bc65546a3a2379f5d1af3a0c52db4f29d4fabaa3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:10:16 -0800 Subject: [PATCH 01/11] feature: add SimpleTaprootChannelsOptional to the set of default chan types --- feature/default_sets.go | 4 ++++ feature/deps.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/feature/default_sets.go b/feature/default_sets.go index 1b1fd1f104a..cefa1c0bc42 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -83,4 +83,8 @@ var defaultSetDesc = setDesc{ SetInit: {}, // I SetNodeAnn: {}, // N }, + lnwire.SimpleTaprootChannelsOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, } diff --git a/feature/deps.go b/feature/deps.go index 8e1d8ac095c..314dd3a6d30 100644 --- a/feature/deps.go +++ b/feature/deps.go @@ -75,6 +75,10 @@ var deps = depDesc{ lnwire.ZeroConfOptional: { lnwire.ScidAliasOptional: {}, }, + lnwire.SimpleTaprootChannelsOptional: { + lnwire.AnchorsZeroFeeHtlcTxOptional: {}, + lnwire.ExplicitChannelTypeOptional: {}, + }, } // ValidateDeps asserts that a feature vector sets all features and their From 1804e0365f924160ed174cb04a69ca99bcf7e5e1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:12:37 -0800 Subject: [PATCH 02/11] funding: update explicitNegotiateCommitmentType to recognize taproot chans --- funding/commitment_type_negotiation.go | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/funding/commitment_type_negotiation.go b/funding/commitment_type_negotiation.go index 475f59a6f30..3a45cd46006 100644 --- a/funding/commitment_type_negotiation.go +++ b/funding/commitment_type_negotiation.go @@ -223,6 +223,52 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local, } return lnwallet.CommitmentTypeTweakless, nil + // Simple taproot channels only. + case channelFeatures.OnlyContains(lnwire.SimpleTaprootChannelsRequired): + + if !hasFeatures( + local, remote, lnwire.SimpleTaprootChannelsOptional, + ) { + + return 0, errUnsupportedChannelType + } + + return lnwallet.CommitmentTypeSimpleTaproot, nil + + // Simple taproot channels with scid only. + case channelFeatures.OnlyContains( + lnwire.SimpleTaprootChannelsRequired, + lnwire.ScidAliasRequired, + ): + + if !hasFeatures( + local, remote, + lnwire.SimpleTaprootChannelsOptional, + lnwire.ScidAliasOptional, + ) { + + return 0, errUnsupportedChannelType + } + + return lnwallet.CommitmentTypeSimpleTaproot, nil + + // Simple taproot channels with zero conf only. + case channelFeatures.OnlyContains( + lnwire.SimpleTaprootChannelsRequired, + lnwire.ZeroConfRequired, + ): + + if !hasFeatures( + local, remote, + lnwire.SimpleTaprootChannelsOptional, + lnwire.ZeroConfOptional, + ) { + + return 0, errUnsupportedChannelType + } + + return lnwallet.CommitmentTypeSimpleTaproot, nil + // No features, use legacy commitment type. case channelFeatures.IsEmpty(): return lnwallet.CommitmentTypeLegacy, nil From a6248ecee6aa4b28f41c23c0ddf40827296bbaf1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:26:05 -0800 Subject: [PATCH 03/11] funding+peer: add support for new musig2 channel funding flow In this commit, we add support for the new musig2 channel funding flow. This flow is identical to the existing flow, but not both sides need to exchange local nonces up front, and then signatures sent are now partial signatures instead of regular signatures. The funding manager also gains some new state of the local nonces it needs to generate in order to send the funding locked message, and also process the funding locked message from the remote party. In order to allow the funding manger to generate the nonces that need to be applied to each channel, then AddNewChannel method has been modified to accept a set of options that the peer will then use to bind the nonces to a new channel. --- discovery/mock_test.go | 5 +- funding/manager.go | 362 +++++++++++++++++++++++++++++++++++----- funding/manager_test.go | 4 +- htlcswitch/link_test.go | 2 +- htlcswitch/mock.go | 2 +- lnpeer/peer.go | 13 +- peer/brontide.go | 18 +- 7 files changed, 348 insertions(+), 58 deletions(-) diff --git a/discovery/mock_test.go b/discovery/mock_test.go index 0040ab8b663..7ddfd2a60bd 100644 --- a/discovery/mock_test.go +++ b/discovery/mock_test.go @@ -7,7 +7,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnwire" ) @@ -42,7 +41,9 @@ func (p *mockPeer) SendMessageLazy(sync bool, msgs ...lnwire.Message) error { return p.SendMessage(sync, msgs...) } -func (p *mockPeer) AddNewChannel(_ *channeldb.OpenChannel, _ <-chan struct{}) error { +func (p *mockPeer) AddNewChannel(_ *lnpeer.NewChannel, + _ <-chan struct{}) error { + return nil } func (p *mockPeer) WipeChannel(_ *wire.OutPoint) {} diff --git a/funding/manager.go b/funding/manager.go index 2d702ba4821..ed8e6edef01 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" @@ -537,6 +538,17 @@ type Manager struct { nonceMtx sync.RWMutex chanIDNonce uint64 + // pendingMusigNonces is used to store the musig2 nonce we generate to + // send funding locked until we receive a funding locked message from + // the remote party. We'll use this to keep track of the nonce we + // generated, so we send the local+remote nonces to the peer state + // machine. + // + // NOTE: This map is protected by the nonceMtx above. + // + // TODO(roasbeef): replace w/ generic concurrent map + pendingMusigNonces map[lnwire.ChannelID]*musig2.Nonces + // activeReservations is a map which houses the state of all pending // funding workflows. activeReservations map[serializedPubKey]pendingChannels @@ -626,7 +638,10 @@ func NewFundingManager(cfg Config) (*Manager, error) { fundingRequests: make(chan *InitFundingMsg, msgBufferSize), localDiscoverySignals: make(map[lnwire.ChannelID]chan struct{}), handleFundingLockedBarriers: make(map[lnwire.ChannelID]struct{}), - quit: make(chan struct{}), + pendingMusigNonces: make( + map[lnwire.ChannelID]*musig2.Nonces, + ), + quit: make(chan struct{}), }, nil } @@ -1052,6 +1067,12 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel, return nil } + // Since we've sent+received funding locked at this point, we + // can clean up the pending musig2 nonce state. + f.nonceMtx.Lock() + delete(f.pendingMusigNonces, chanID) + f.nonceMtx.Unlock() + var peerAlias *lnwire.ShortChannelID if channel.IsZeroConf() { // We'll need to wait until funding_locked has been @@ -1493,15 +1514,25 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, } } + public := msg.ChannelFlags&lnwire.FFAnnounceChannel != 0 + switch { // Sending the option-scid-alias channel type for a public channel is // disallowed. - public := msg.ChannelFlags&lnwire.FFAnnounceChannel != 0 - if public && scid { + case public && scid: err = fmt.Errorf("option-scid-alias chantype for public " + "channel") log.Error(err) f.failFundingFlow(peer, msg.PendingChannelID, err) return + + // The current variant of taproot channels can only be used with + // unadvertised channels for now. + case commitType.IsTaproot() && public: + err = fmt.Errorf("taproot channel type for public channel") + log.Error(err) + f.failFundingFlow(peer, msg.PendingChannelID, err) + + return } req := &lnwallet.InitFundingReserveMsg{ @@ -1740,6 +1771,22 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, }, UpfrontShutdown: msg.UpfrontShutdownScript, } + + if resCtx.reservation.IsTaproot() { + if msg.LocalNonce == nil { + err := fmt.Errorf("local nonce not set for taproot " + + "chan") + log.Error(err) + f.failFundingFlow( + resCtx.peer, msg.PendingChannelID, err, + ) + } + + remoteContribution.LocalNonce = &musig2.Nonces{ + PubNonce: *msg.LocalNonce, + } + } + err = reservation.ProcessSingleContribution(remoteContribution) if err != nil { log.Errorf("unable to add contribution reservation: %v", err) @@ -1752,9 +1799,17 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, log.Debugf("Remote party accepted commitment constraints: %v", spew.Sdump(remoteContribution.ChannelConfig.ChannelConstraints)) + ourContribution := reservation.OurContribution() + + var localNonce *lnwire.Musig2Nonce + if commitType.IsTaproot() { + localNonce = (*lnwire.Musig2Nonce)( + &ourContribution.LocalNonce.PubNonce, + ) + } + // With the initiator's contribution recorded, respond with our // contribution in the next message of the workflow. - ourContribution := reservation.OurContribution() fundingAccept := lnwire.AcceptChannel{ PendingChannelID: msg.PendingChannelID, DustLimit: ourContribution.DustLimit, @@ -1773,6 +1828,7 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, UpfrontShutdownScript: ourContribution.UpfrontShutdown, ChannelType: chanTypeFeatureBits, LeaseExpiry: msg.LeaseExpiry, + LocalNonce: localNonce, } if err := peer.SendMessage(true, &fundingAccept); err != nil { @@ -1965,6 +2021,20 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer, }, UpfrontShutdown: msg.UpfrontShutdownScript, } + + if resCtx.reservation.IsTaproot() { + if msg.LocalNonce == nil { + err := fmt.Errorf("local nonce not set for taproot " + + "chan") + log.Error(err) + f.failFundingFlow(resCtx.peer, pendingChanID, err) + } + + remoteContribution.LocalNonce = &musig2.Nonces{ + PubNonce: *msg.LocalNonce, + } + } + err = resCtx.reservation.ProcessContribution(remoteContribution) // The wallet has detected that a PSBT funding process was requested by @@ -2147,12 +2217,30 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx, PendingChannelID: pendingChanID, FundingPoint: *outPoint, } - fundingCreated.CommitSig, err = lnwire.NewSigFromSignature(sig) - if err != nil { - log.Errorf("Unable to parse signature: %v", err) - f.failFundingFlow(resCtx.peer, pendingChanID, err) - return + + // If this is a taproot channel, then we'll need to populate the musig2 + // partial sig field instead of the regular commit sig field. + if resCtx.reservation.IsTaproot() { + partialSig, ok := sig.(*lnwallet.MusigPartialSig) + if !ok { + err := fmt.Errorf("expected musig partial sig, got %T", + sig) + log.Error(err) + f.failFundingFlow(resCtx.peer, pendingChanID, err) + + return + } + + fundingCreated.PartialSig = partialSig.ToWireSig() + } else { + fundingCreated.CommitSig, err = lnwire.NewSigFromSignature(sig) + if err != nil { + log.Errorf("Unable to parse signature: %v", err) + f.failFundingFlow(resCtx.peer, pendingChanID, err) + return + } } + if err := resCtx.peer.SendMessage(true, fundingCreated); err != nil { log.Errorf("Unable to send funding complete message: %v", err) f.failFundingFlow(resCtx.peer, pendingChanID, err) @@ -2186,11 +2274,27 @@ func (f *Manager) handleFundingCreated(peer lnpeer.Peer, log.Infof("completing pending_id(%x) with ChannelPoint(%v)", pendingChanID[:], fundingOut) - commitSig, err := msg.CommitSig.ToSignature() - if err != nil { - log.Errorf("unable to parse signature: %v", err) - f.failFundingFlow(peer, pendingChanID, err) - return + // For taproot channels, the commit signature is actually the partial + // signature. Otherwise, we can convert the ECDSA commit signature into + // our internal input.Signature type. + var commitSig input.Signature + if resCtx.reservation.IsTaproot() { + if msg.PartialSig == nil { + log.Errorf("partial sig not included: %v", err) + f.failFundingFlow(peer, pendingChanID, err) + return + } + + commitSig = new(lnwallet.MusigPartialSig).FromWireSig( + msg.PartialSig, + ) + } else { + commitSig, err = msg.CommitSig.ToSignature() + if err != nil { + log.Errorf("unable to parse signature: %v", err) + f.failFundingFlow(peer, pendingChanID, err) + return + } } // With all the necessary data available, attempt to advance the @@ -2255,21 +2359,39 @@ func (f *Manager) handleFundingCreated(peer lnpeer.Peer, log.Infof("sending FundingSigned for pending_id(%x) over "+ "ChannelPoint(%v)", pendingChanID[:], fundingOut) - // With their signature for our version of the commitment transaction - // verified, we can now send over our signature to the remote peer. - _, sig := resCtx.reservation.OurSignatures() - ourCommitSig, err := lnwire.NewSigFromSignature(sig) - if err != nil { - log.Errorf("unable to parse signature: %v", err) - f.failFundingFlow(peer, pendingChanID, err) - deleteFromDatabase() - return + fundingSigned := &lnwire.FundingSigned{ + ChanID: channelID, } - fundingSigned := &lnwire.FundingSigned{ - ChanID: channelID, - CommitSig: ourCommitSig, + // For taproot channels, we'll need to send over a partial signature + // that includes the nonce along side the signature. + _, sig := resCtx.reservation.OurSignatures() + if resCtx.reservation.IsTaproot() { + partialSig, ok := sig.(*lnwallet.MusigPartialSig) + if !ok { + err := fmt.Errorf("expected musig partial sig, got %T", + sig) + log.Error(err) + f.failFundingFlow(resCtx.peer, pendingChanID, err) + deleteFromDatabase() + + return + } + + fundingSigned.PartialSig = partialSig.ToWireSig() + } else { + fundingSigned.CommitSig, err = lnwire.NewSigFromSignature(sig) + if err != nil { + log.Errorf("unable to parse signature: %v", err) + f.failFundingFlow(peer, pendingChanID, err) + deleteFromDatabase() + + return + } } + + // With their signature for our version of the commitment transaction + // verified, we can now send over our signature to the remote peer. if err := peer.SendMessage(true, fundingSigned); err != nil { log.Errorf("unable to send FundingSigned message: %v", err) f.failFundingFlow(peer, pendingChanID, err) @@ -2374,14 +2496,27 @@ func (f *Manager) handleFundingSigned(peer lnpeer.Peer, log.Errorf("Unable to store the forwarding policy: %v", err) } - // The remote peer has responded with a signature for our commitment - // transaction. We'll verify the signature for validity, then commit - // the state to disk as we can now open the channel. - commitSig, err := msg.CommitSig.ToSignature() - if err != nil { - log.Errorf("Unable to parse signature: %v", err) - f.failFundingFlow(peer, pendingChanID, err) - return + // For taproot channels, the commit signature is actually the partial + // signature. Otherwise, we can convert the ECDSA commit signature into + // our internal input.Signature type. + var commitSig input.Signature + if resCtx.reservation.IsTaproot() { + if msg.PartialSig == nil { + log.Errorf("partial sig not included: %v", err) + f.failFundingFlow(peer, pendingChanID, err) + return + } + + commitSig = new(lnwallet.MusigPartialSig).FromWireSig( + msg.PartialSig, + ) + } else { + commitSig, err = msg.CommitSig.ToSignature() + if err != nil { + log.Errorf("unable to parse signature: %v", err) + f.failFundingFlow(peer, pendingChanID, err) + return + } } completeChan, err := resCtx.reservation.CompleteReservation( @@ -2610,10 +2745,24 @@ func (f *Manager) waitForFundingWithTimeout( // makeFundingScript re-creates the funding script for the funding transaction // of the target channel. func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) { - localKey := channel.LocalChanCfg.MultiSigKey.PubKey.SerializeCompressed() - remoteKey := channel.RemoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() + localKey := channel.LocalChanCfg.MultiSigKey.PubKey + remoteKey := channel.RemoteChanCfg.MultiSigKey.PubKey - multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey) + if channel.ChanType.IsTaproot() { + pkScript, _, err := input.GenTaprootFundingScript( + localKey, remoteKey, int64(channel.Capacity), + ) + if err != nil { + return nil, err + } + + return pkScript, nil + } + + multiSigScript, err := input.GenMultiSigScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), + ) if err != nil { return nil, err } @@ -2911,6 +3060,38 @@ func (f *Manager) sendFundingLocked(completeChan *channeldb.OpenChannel, } fundingLockedMsg := lnwire.NewFundingLocked(chanID, nextRevocation) + // If this is a taproot channel, then we also need to send along our + // set of musig2 nonces as well. + if completeChan.ChanType.IsTaproot() { + log.Infof("ChanID(%v): generating musig2 nonces...", + chanID) + + f.nonceMtx.Lock() + localNonce, ok := f.pendingMusigNonces[chanID] + if !ok { + // If we don't have any nonces generated yet for this + // first state, then we'll generate them now and stow + // them away. When we receive the funding locked + // message, we'll then pass along this same set of + // nonces. + newNonce, err := channel.GenMusigNonces() + if err != nil { + f.nonceMtx.Unlock() + return err + } + + // Now that we've generated the nonce for this channel, + // we'll store it in the set of pending nonces. + localNonce = newNonce + f.pendingMusigNonces[chanID] = localNonce + } + f.nonceMtx.Unlock() + + fundingLockedMsg.NextLocalNonce = (*lnwire.Musig2Nonce)( + &localNonce.PubNonce, + ) + } + // If the channel negotiated the option-scid-alias feature bit, we'll // send a TLV segment that includes an alias the peer can use in their // invoice hop hints. We'll send the first alias we find for the @@ -3087,6 +3268,7 @@ func (f *Manager) addToRouterGraph(completeChan *channeldb.OpenChannel, &completeChan.LocalChanCfg.MultiSigKey, completeChan.RemoteChanCfg.MultiSigKey.PubKey, *shortChanID, chanID, fwdMinHTLC, fwdMaxHTLC, ourPolicy, + completeChan.ChanType, ) if err != nil { return fmt.Errorf("error generating channel "+ @@ -3287,7 +3469,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel, f.cfg.IDKey, completeChan.IdentityPub, &completeChan.LocalChanCfg.MultiSigKey, completeChan.RemoteChanCfg.MultiSigKey.PubKey, - *shortChanID, chanID, + *shortChanID, chanID, completeChan.ChanType, ) if err != nil { return fmt.Errorf("channel announcement failed: %w", @@ -3391,6 +3573,35 @@ func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel, return nil } +// genFirstStateMusigNonce generates a nonces for the "first" local state. This +// is the verification nonce for the state created for us after the initial +// commitment transaction signed as part of the funding flow. +func genFirstStateMusigNonce(channel *channeldb.OpenChannel, +) (*musig2.Nonces, error) { + + musig2ShaChain, err := channeldb.DeriveMusig2Shachain( + channel.RevocationProducer, + ) + if err != nil { + return nil, fmt.Errorf("unable to generate musig channel "+ + "nonces: %v", err) + } + + // We use the _next_ commitment height here as we need to generate the + // nonce for the next state the remote party will sign for us. + verNonce, err := channeldb.NewMusigVerificationNonce( + channel.LocalChanCfg.MultiSigKey.PubKey, + channel.LocalCommitment.CommitHeight+1, + musig2ShaChain, + ) + if err != nil { + return nil, fmt.Errorf("unable to generate musig channel "+ + "nonces: %v", err) + } + + return verNonce, nil +} + // handleFundingLocked finalizes the channel funding process and enables the // channel to enter normal operating mode. func (f *Manager) handleFundingLocked(peer lnpeer.Peer, @@ -3459,6 +3670,17 @@ func (f *Manager) handleFundingLocked(peer lnpeer.Peer, return } + // If this is a taproot channel, then we can generate the set of nonces + // the remote party needs to send the next remote commitment here. + var firstVerNonce *musig2.Nonces + if channel.ChanType.IsTaproot() { + firstVerNonce, err = genFirstStateMusigNonce(channel) + if err != nil { + log.Error(err) + return + } + } + // We'll need to store the received TLV alias if the option_scid_alias // feature was negotiated. This will be used to provide route hints // during invoice creation. In the zero-conf case, it is also used to @@ -3522,6 +3744,14 @@ func (f *Manager) handleFundingLocked(peer lnpeer.Peer, ) fundingLockedMsg.AliasScid = &alias + if firstVerNonce != nil { + wireNonce := (*lnwire.Musig2Nonce)( + &firstVerNonce.PubNonce, + ) + + fundingLockedMsg.NextLocalNonce = wireNonce + } + err = peer.SendMessage(true, fundingLockedMsg) if err != nil { log.Errorf("unable to send funding locked: %v", @@ -3546,6 +3776,38 @@ func (f *Manager) handleFundingLocked(peer lnpeer.Peer, return } + // If this is a taproot channel, then we'll need to map the received + // nonces to a nonce pair, and also fetch our pending nonces, which are + // required in order to make the channel whole. + var chanOpts []lnwallet.ChannelOpt + if channel.ChanType.IsTaproot() { + f.nonceMtx.Lock() + localNonce, ok := f.pendingMusigNonces[chanID] + if !ok { + // If there's no pending nonce for this channel ID, + // we'll use the one generatd above. + localNonce = firstVerNonce + f.pendingMusigNonces[chanID] = firstVerNonce + } + f.nonceMtx.Unlock() + + log.Infof("ChanID(%v): applying local+remote musig2 nonces", + chanID) + + if msg.NextLocalNonce == nil { + log.Errorf("remote nonces are nil") + return + } + + chanOpts = append( + chanOpts, + lnwallet.WithLocalMusigNonces(localNonce), + lnwallet.WithRemoteMusigNonces(&musig2.Nonces{ + PubNonce: *msg.NextLocalNonce, + }), + ) + } + // The funding locked message contains the next commitment point we'll // need to create the next commitment state for the remote party. So // we'll insert that into the channel now before passing it along to @@ -3573,7 +3835,11 @@ func (f *Manager) handleFundingLocked(peer lnpeer.Peer, f.barrierMtx.Unlock() }() - if err := peer.AddNewChannel(channel, f.quit); err != nil { + err = peer.AddNewChannel(&lnpeer.NewChannel{ + OpenChannel: channel, + ChanOpts: chanOpts, + }, f.quit) + if err != nil { log.Errorf("Unable to add new channel %v with peer %x: %v", channel.FundingOutpoint, peer.IdentityKey().SerializeCompressed(), err, @@ -3600,9 +3866,9 @@ type chanAnnouncement struct { func (f *Manager) newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey, localFundingKey *keychain.KeyDescriptor, remoteFundingKey *btcec.PublicKey, shortChanID lnwire.ShortChannelID, - chanID lnwire.ChannelID, fwdMinHTLC, - fwdMaxHTLC lnwire.MilliSatoshi, - ourPolicy *channeldb.ChannelEdgePolicy) (*chanAnnouncement, error) { + chanID lnwire.ChannelID, fwdMinHTLC, fwdMaxHTLC lnwire.MilliSatoshi, + ourPolicy *channeldb.ChannelEdgePolicy, + chanType channeldb.ChannelType) (*chanAnnouncement, error) { chainHash := *f.cfg.Wallet.Cfg.NetParams.GenesisHash @@ -3793,7 +4059,7 @@ func (f *Manager) newChanAnnouncement(localPubKey, func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, localFundingKey *keychain.KeyDescriptor, remoteFundingKey *btcec.PublicKey, shortChanID lnwire.ShortChannelID, - chanID lnwire.ChannelID) error { + chanID lnwire.ChannelID, chanType channeldb.ChannelType) error { // First, we'll create the batch of announcements to be sent upon // initial channel creation. This includes the channel announcement @@ -3804,7 +4070,7 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, // only use the channel announcement message from the returned struct. ann, err := f.newChanAnnouncement(localIDKey, remoteIDKey, localFundingKey, remoteFundingKey, shortChanID, chanID, - 0, 0, nil, + 0, 0, nil, chanType, ) if err != nil { log.Errorf("can't generate channel announcement: %v", err) @@ -4233,6 +4499,13 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { log.Infof("Starting funding workflow with %v for pending_id(%x), "+ "committype=%v", msg.Peer.Address(), chanID, commitType) + var localNonce *lnwire.Musig2Nonce + if commitType.IsTaproot() { + localNonce = (*lnwire.Musig2Nonce)( + &ourContribution.LocalNonce.PubNonce, + ) + } + fundingOpen := lnwire.OpenChannel{ ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash, PendingChannelID: chanID, @@ -4255,6 +4528,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { UpfrontShutdownScript: shutdown, ChannelType: chanType, LeaseExpiry: leaseExpiry, + LocalNonce: localNonce, } if err := msg.Peer.SendMessage(true, &fundingOpen); err != nil { e := fmt.Errorf("unable to send funding request message: %v", diff --git a/funding/manager_test.go b/funding/manager_test.go index f386e3de58b..05ba8e93eae 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -233,7 +233,7 @@ func (m *mockZeroConfAcceptor) Accept( } type newChannelMsg struct { - channel *channeldb.OpenChannel + channel *lnpeer.NewChannel err chan error } @@ -297,7 +297,7 @@ func (n *testNode) RemoteFeatures() *lnwire.FeatureVector { ) } -func (n *testNode) AddNewChannel(channel *channeldb.OpenChannel, +func (n *testNode) AddNewChannel(channel *lnpeer.NewChannel, quit <-chan struct{}) error { errChan := make(chan error) diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index ba1cc38716e..1279622339b 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1770,7 +1770,7 @@ func (m *mockPeer) SendMessage(sync bool, msgs ...lnwire.Message) error { func (m *mockPeer) SendMessageLazy(sync bool, msgs ...lnwire.Message) error { return m.SendMessage(sync, msgs...) } -func (m *mockPeer) AddNewChannel(_ *channeldb.OpenChannel, +func (m *mockPeer) AddNewChannel(_ *lnpeer.NewChannel, _ <-chan struct{}) error { return nil } diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index 866e7d36f39..69de7757cc4 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -666,7 +666,7 @@ func (s *mockServer) Address() net.Addr { return nil } -func (s *mockServer) AddNewChannel(channel *channeldb.OpenChannel, +func (s *mockServer) AddNewChannel(channel *lnpeer.NewChannel, cancel <-chan struct{}) error { return nil diff --git a/lnpeer/peer.go b/lnpeer/peer.go index 465a41cb903..7a54e1e5cf6 100644 --- a/lnpeer/peer.go +++ b/lnpeer/peer.go @@ -6,9 +6,20 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" ) +// NewChannel is a newly funded channel. This struct couples a channel along +// with the set of channel options that may change how the channel is created. +// This can be used to pass along the nonce state needed for taproot channels. +type NewChannel struct { + *channeldb.OpenChannel + + // ChanOpts can be used to change how the channel is created. + ChanOpts []lnwallet.ChannelOpt +} + // Peer is an interface which represents a remote lightning node. type Peer interface { // SendMessage sends a variadic number of high-priority message to @@ -25,7 +36,7 @@ type Peer interface { // AddNewChannel adds a new channel to the peer. The channel should fail // to be added if the cancel channel is closed. - AddNewChannel(channel *channeldb.OpenChannel, cancel <-chan struct{}) error + AddNewChannel(newChan *NewChannel, cancel <-chan struct{}) error // WipeChannel removes the channel uniquely identified by its channel // point from all indexes associated with the peer. diff --git a/peer/brontide.go b/peer/brontide.go index 2330cc464dd..24d1a650252 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -87,7 +87,7 @@ type outgoingMsg struct { // the receiver of the request to report when the channel creation process has // completed. type newChannelMsg struct { - channel *channeldb.OpenChannel + channel *lnpeer.NewChannel err chan error } @@ -2365,9 +2365,10 @@ out: chanPoint := &newChan.FundingOutpoint chanID := lnwire.NewChanIDFromOutPoint(chanPoint) - // Only update RemoteNextRevocation if the channel is in the - // activeChannels map and if we added the link to the switch. - // Only active channels will be added to the switch. + // Only update RemoteNextRevocation if the channel is + // in the activeChannels map and if we added the link + // to the switch. Only active channels will be added + // to the switch. p.activeChanMtx.Lock() currentChan, ok := p.activeChannels[chanID] if ok && currentChan != nil { @@ -2397,6 +2398,8 @@ out: continue } + // TODO(roasbeef): don't also need to apply + // nonces here? get from chan reest continue } @@ -2404,7 +2407,8 @@ out: // set of active channels, so we can look it up later // easily according to its channel ID. lnChan, err := lnwallet.NewLightningChannel( - p.cfg.Signer, newChan, p.cfg.SigPool, + p.cfg.Signer, newChan.OpenChannel, + p.cfg.SigPool, newChan.ChanOpts..., ) if err != nil { p.activeChanMtx.Unlock() @@ -3548,12 +3552,12 @@ func (p *Brontide) Address() net.Addr { // added if the cancel channel is closed. // // NOTE: Part of the lnpeer.Peer interface. -func (p *Brontide) AddNewChannel(channel *channeldb.OpenChannel, +func (p *Brontide) AddNewChannel(newChan *lnpeer.NewChannel, cancel <-chan struct{}) error { errChan := make(chan error, 1) newChanMsg := &newChannelMsg{ - channel: channel, + channel: newChan, err: errChan, } From 83fe89645736b73d5fc013fc3afccbd2e11b67d1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:29:50 -0800 Subject: [PATCH 04/11] routing+funding: add new makeFundingScript to support reg and taproot channels In this commit, we start to set _internally_ a new feature bit in the channel announcements we generate. As these taproot channels can only be unadvertised, this will never actually leak to the public network. The funding manager will then set this field to allow the router to properly validate these channels. --- funding/manager.go | 12 ++++++++ routing/router.go | 74 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index ed8e6edef01..b1bdfa608a3 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -3881,6 +3881,18 @@ func (f *Manager) newChanAnnouncement(localPubKey, ChainHash: chainHash, } + // If this is a taproot channel, then we'll set a special bit in the + // feature vector to indicate to the routing layer that this needs a + // slightly different type of validation. + // + // TODO(roasbeef): temp, remove after gossip 1.5 + if chanType.IsTaproot() { + log.Debugf("Applying taproot feature bit to "+ + "ChannelAnnouncement for %v", chanID) + + chanAnn.Features.Set(lnwire.SimpleTaprootChannelsRequired) + } + // The chanFlags field indicates which directed edge of the channel is // being updated within the ChannelUpdateAnnouncement announcement // below. A value of zero means it's the edge of the "first" node and 1 diff --git a/routing/router.go b/routing/router.go index 5ecce6646e8..cb835f57fd5 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1411,6 +1411,67 @@ func (r *ChannelRouter) addZombieEdge(chanID uint64) error { return nil } +// makeFundingScript is used to make the funding script for both segwit v0 and +// segwit v1 (taproot) channels. +// +// TODO(roasbeef: export and use elsewhere? +func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, + chanFeatures []byte) ([]byte, error) { + + legacyFundingScript := func() ([]byte, error) { + witnessScript, err := input.GenMultiSigScript( + bitcoinKey1, bitcoinKey2, + ) + if err != nil { + return nil, err + } + pkScript, err := input.WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + + return pkScript, nil + } + + if len(chanFeatures) == 0 { + return legacyFundingScript() + } + + // In order to make the correct funding script, we'll need to parse the + // chanFeatures bytes into a feature vector we can interact with. + rawFeatures := lnwire.NewRawFeatureVector() + err := rawFeatures.Decode(bytes.NewReader(chanFeatures)) + if err != nil { + return nil, fmt.Errorf("unable to parse chan feature "+ + "bits: %w", err) + } + + chanFeatureBits := lnwire.NewFeatureVector( + rawFeatures, lnwire.Features, + ) + if chanFeatureBits.HasFeature(lnwire.SimpleTaprootChannelsOptional) { + pubKey1, err := btcec.ParsePubKey(bitcoinKey1) + if err != nil { + return nil, err + } + pubKey2, err := btcec.ParsePubKey(bitcoinKey2) + if err != nil { + return nil, err + } + + fundingScript, _, err := input.GenTaprootFundingScript( + pubKey1, pubKey2, 0, + ) + if err != nil { + return nil, err + } + + return fundingScript, nil + } + + return legacyFundingScript() +} + // processUpdate processes a new relate authenticated channel/edge, node or // channel/edge update network update. If the update didn't affect the internal // state of the draft due to either being out of date, invalid, or redundant, @@ -1520,16 +1581,13 @@ func (r *ChannelRouter) processUpdate(msg interface{}, // Recreate witness output to be sure that declared in channel // edge bitcoin keys and channel value corresponds to the // reality. - witnessScript, err := input.GenMultiSigScript( + fundingPkScript, err := makeFundingScript( msg.BitcoinKey1Bytes[:], msg.BitcoinKey2Bytes[:], + msg.Features, ) if err != nil { return err } - pkScript, err := input.WitnessScriptHash(witnessScript) - if err != nil { - return err - } // Next we'll validate that this channel is actually well // formed. If this check fails, then this channel either @@ -1539,7 +1597,7 @@ func (r *ChannelRouter) processUpdate(msg interface{}, Locator: &chanvalidate.ShortChanIDChanLocator{ ID: channelID, }, - MultiSigPkScript: pkScript, + MultiSigPkScript: fundingPkScript, FundingTx: fundingTx, }) if err != nil { @@ -1556,10 +1614,6 @@ func (r *ChannelRouter) processUpdate(msg interface{}, // Now that we have the funding outpoint of the channel, ensure // that it hasn't yet been spent. If so, then this channel has // been closed so we'll ignore it. - fundingPkScript, err := input.WitnessScriptHash(witnessScript) - if err != nil { - return err - } chanUtxo, err := r.cfg.Chain.GetUtxo( fundingPoint, fundingPkScript, channelID.BlockHeight, r.quit, From b66d538cfde945cbfd2ff367ccd9e7e17cb21ef1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:30:10 -0800 Subject: [PATCH 05/11] htlcswitch: add awareness of new partial sig fields and musig2 nonces --- htlcswitch/link.go | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 959792881d8..8cd08224d6f 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "time" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" @@ -772,6 +773,26 @@ func (l *channelLink) syncChanStates() error { } } + // Before we process the ChanSync message, if this is a taproot + // channel, then we'll init our musig2 nonces state. + if chanState.ChanType.IsTaproot() { + l.log.Infof("initializing musig2 nonces") + + if remoteChanSyncMsg.LocalNonce == nil { + return fmt.Errorf("remote nonce is nil") + } + + syncMsg := remoteChanSyncMsg + remoteNonce := &musig2.Nonces{ + PubNonce: *syncMsg.LocalNonce, + } + err := l.channel.InitRemoteMusigNonces(remoteNonce) + if err != nil { + return fmt.Errorf("unable to init musig2 "+ + "nonces: %w", err) + } + } + // In any case, we'll then process their ChanSync message. l.log.Info("received re-establishment message from remote side") @@ -1936,8 +1957,9 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // chain, validate this new commitment, closing the link if // invalid. err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{ - CommitSig: msg.CommitSig, - HtlcSigs: msg.HtlcSigs, + CommitSig: msg.CommitSig, + HtlcSigs: msg.HtlcSigs, + PartialSig: msg.PartialSig, }) if err != nil { // If we were unable to reconstruct their proposed @@ -2315,9 +2337,10 @@ func (l *channelLink) updateCommitTx() error { } commitSig := &lnwire.CommitSig{ - ChanID: l.ChanID(), - CommitSig: newCommit.CommitSig, - HtlcSigs: newCommit.HtlcSigs, + ChanID: l.ChanID(), + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, + PartialSig: newCommit.PartialSig, } l.cfg.Peer.SendMessage(false, commitSig) From e46dae549617f8aee5fa1c0d8d0c298cee7281a3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 20:31:31 -0800 Subject: [PATCH 06/11] contractcourt: update chain watcher to understand the taproot pkscript In this commit, we update the chain watcher to be able to generate the correct pkScript so it can register for confirmation and spend notifications for taproot channels. --- contractcourt/chain_watcher.go | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 7fd45e1c244..39d160538ca 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -287,17 +287,32 @@ func (c *chainWatcher) Start() error { } } - localKey := chanState.LocalChanCfg.MultiSigKey.PubKey.SerializeCompressed() - remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() - multiSigScript, err := input.GenMultiSigScript( - localKey, remoteKey, + localKey := chanState.LocalChanCfg.MultiSigKey.PubKey + remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey + + var ( + pkScript []byte + err error ) - if err != nil { - return err - } - pkScript, err := input.WitnessScriptHash(multiSigScript) - if err != nil { - return err + if chanState.ChanType.IsTaproot() { + pkScript, _, err = input.GenTaprootFundingScript( + localKey, remoteKey, 0, + ) + if err != nil { + return err + } + } else { + multiSigScript, err := input.GenMultiSigScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), + ) + if err != nil { + return err + } + pkScript, err = input.WitnessScriptHash(multiSigScript) + if err != nil { + return err + } } spendNtfn, err := c.cfg.notifier.RegisterSpendNtfn( @@ -833,6 +848,9 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, } // Create an AnchorResolution for the breached state. + // + // TODO(roasbeef): make keyring for taproot chans to pass in instead of + // nil anchorRes, err := lnwallet.NewAnchorResolution( c.cfg.chanState, commitSpend.SpendingTx, nil, ) From 3555afbe983f9e55d3cb0127d57e404245a9d060 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 24 Jul 2023 21:20:57 +0200 Subject: [PATCH 07/11] lnwallet: generate local nonces if non passed in for taproot chans This ensures that when loading the channel again after a normal chan reest, we generate the local nonces, which ensures we can then process nonces the remote party sends us in their chan reest message. --- lnwallet/channel.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 4fa9d1b6598..a5e7bbf9e80 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1423,10 +1423,20 @@ func NewLightningChannel(signer input.Signer, log: build.NewPrefixLog(logPrefix, walletLog), } + switch { // At this point, we may already have nonces that were passed in, so // we'll check that now as this lets us skip some steps later. - if opts.localNonce != nil { + case state.ChanType.IsTaproot() && opts.localNonce != nil: lc.pendingVerificationNonce = opts.localNonce + + // Otherwise, we'll generate the nonces here ourselves. This ensures + // we'll be ablve to process the chan syncmessag efrom the remote + // party. + case state.ChanType.IsTaproot() && opts.localNonce == nil: + _, err := lc.GenMusigNonces() + if err != nil { + return nil, err + } } if lc.pendingVerificationNonce != nil && opts.remoteNonce != nil { err := lc.InitRemoteMusigNonces(opts.remoteNonce) From ed3becba92da550c28037e893d78800544b32dc7 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 25 Jul 2023 17:10:53 -0700 Subject: [PATCH 08/11] lnwallet: add extra sanity check in VerifyCommitSig --- lnwallet/musig_session.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 89cc36a9607..f850f8cf6f6 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -436,6 +436,10 @@ func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx, optFunc(opts) } + if sig == nil { + return nil, fmt.Errorf("sig not provided") + } + // Before we can verify the signature, we'll need to finalize the // session by binding the remote party's provided signing nonce. if err := m.FinalizeSession(musig2.Nonces{ From b09c3dbc86badb170d57a21fbe68c567dc4aaaf9 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 25 Jul 2023 17:19:47 -0700 Subject: [PATCH 09/11] contractcourt: for taproot channels, wait for conf in closeObserver This ensures that we end up playing the target output on chain for taproot channels. --- contractcourt/chain_watcher.go | 53 +++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 39d160538ca..36a1a96f3a2 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -207,6 +207,13 @@ type chainWatcher struct { // the current state number on the commitment transactions. stateHintObfuscator [lnwallet.StateHintSize]byte + // fundingPkScript is the pkScript of the funding output. + fundingPkScript []byte + + // heightHint is the height hint used to checkpoint scans on chain for + // conf/spend events. + heightHint uint32 + // All the fields below are protected by this mutex. sync.Mutex @@ -267,9 +274,9 @@ func (c *chainWatcher) Start() error { // As a height hint, we'll try to use the opening height, but if the // channel isn't yet open, then we'll use the height it was broadcast // at. This may be an unconfirmed zero-conf channel. - heightHint := c.cfg.chanState.ShortChanID().BlockHeight - if heightHint == 0 { - heightHint = chanState.BroadcastHeight() + c.heightHint = c.cfg.chanState.ShortChanID().BlockHeight + if c.heightHint == 0 { + c.heightHint = chanState.BroadcastHeight() } // Since no zero-conf state is stored in a channel backup, the below @@ -279,11 +286,11 @@ func (c *chainWatcher) Start() error { if chanState.ZeroConfConfirmed() { // If the zero-conf channel is confirmed, we'll use the // confirmed SCID's block height. - heightHint = chanState.ZeroConfRealScid().BlockHeight + c.heightHint = chanState.ZeroConfRealScid().BlockHeight } else { // The zero-conf channel is unconfirmed. We'll need to // use the FundingBroadcastHeight. - heightHint = chanState.BroadcastHeight() + c.heightHint = chanState.BroadcastHeight() } } @@ -291,11 +298,10 @@ func (c *chainWatcher) Start() error { remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey var ( - pkScript []byte - err error + err error ) if chanState.ChanType.IsTaproot() { - pkScript, _, err = input.GenTaprootFundingScript( + c.fundingPkScript, _, err = input.GenTaprootFundingScript( localKey, remoteKey, 0, ) if err != nil { @@ -309,14 +315,14 @@ func (c *chainWatcher) Start() error { if err != nil { return err } - pkScript, err = input.WitnessScriptHash(multiSigScript) + c.fundingPkScript, err = input.WitnessScriptHash(multiSigScript) if err != nil { return err } } spendNtfn, err := c.cfg.notifier.RegisterSpendNtfn( - fundingOut, pkScript, heightHint, + fundingOut, c.fundingPkScript, c.heightHint, ) if err != nil { return err @@ -582,6 +588,33 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) { log.Infof("Close observer for ChannelPoint(%v) active", c.cfg.chanState.FundingOutpoint) + // If this is a taproot channel, before we proceed, we want to ensure + // that the expected funding output has confirmed on chain. + if c.cfg.chanState.ChanType.IsTaproot() { + fundingPoint := c.cfg.chanState.FundingOutpoint + + confNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn( + &fundingPoint.Hash, c.fundingPkScript, 1, c.heightHint, + ) + if err != nil { + log.Warnf("unable to register for conf: %v", err) + } + + log.Infof("Waiting for taproot ChannelPoint(%v) to confirm...", + c.cfg.chanState.FundingOutpoint) + + select { + case _, ok := <-confNtfn.Confirmed: + // If the channel was closed, then this means that the + // notifier exited, so we will as well. + if !ok { + return + } + case <-c.quit: + return + } + } + select { // We've detected a spend of the channel onchain! Depending on the type // of spend, we'll act accordingly, so we'll examine the spending From 7bca374600406edeedf8894b559be0727a348bb0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 26 Jul 2023 17:03:04 -0700 Subject: [PATCH 10/11] lntest: update SingleMockSigner to support musig2 --- lntest/mock/signer.go | 66 +++++++++++-------------------------------- 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/lntest/mock/signer.go b/lntest/mock/signer.go index 7ce6cf4e060..5f21437e716 100644 --- a/lntest/mock/signer.go +++ b/lntest/mock/signer.go @@ -105,6 +105,22 @@ func (d *DummySigner) MuSig2Cleanup(input.MuSig2SessionID) error { type SingleSigner struct { Privkey *btcec.PrivateKey KeyLoc keychain.KeyLocator + + *input.MusigSessionManager +} + +func NewSingleSigner(privkey *btcec.PrivateKey) *SingleSigner { + signer := &SingleSigner{ + Privkey: privkey, + KeyLoc: idKeyLoc, + } + + keyFetcher := func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error) { + return signer.Privkey, nil + } + signer.MusigSessionManager = input.NewMusigSessionManager(keyFetcher) + + return signer } // SignOutputRaw generates a signature for the passed transaction using the @@ -189,53 +205,3 @@ func (s *SingleSigner) SignMessage(keyLoc keychain.KeyLocator, } return ecdsa.Sign(s.Privkey, digest), nil } - -// MuSig2CreateSession creates a new MuSig2 signing session using the local -// key identified by the key locator. The complete list of all public keys of -// all signing parties must be provided, including the public key of the local -// signing key. If nonces of other parties are already known, they can be -// submitted as well to reduce the number of method calls necessary later on. -func (s *SingleSigner) MuSig2CreateSession(input.MuSig2Version, - keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, - [][musig2.PubNonceSize]byte, - ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) { - - return nil, nil -} - -// MuSig2RegisterNonces registers one or more public nonces of other signing -// participants for a session identified by its ID. This method returns true -// once we have all nonces for all other signing participants. -func (s *SingleSigner) MuSig2RegisterNonces(input.MuSig2SessionID, - [][musig2.PubNonceSize]byte) (bool, error) { - - return false, nil -} - -// MuSig2Sign creates a partial signature using the local signing key -// that was specified when the session was created. This can only be -// called when all public nonces of all participants are known and have -// been registered with the session. If this node isn't responsible for -// combining all the partial signatures, then the cleanup parameter -// should be set, indicating that the session can be removed from memory -// once the signature was produced. -func (s *SingleSigner) MuSig2Sign(input.MuSig2SessionID, - [sha256.Size]byte, bool) (*musig2.PartialSignature, error) { - - return nil, nil -} - -// MuSig2CombineSig combines the given partial signature(s) with the -// local one, if it already exists. Once a partial signature of all -// participants is registered, the final signature will be combined and -// returned. -func (s *SingleSigner) MuSig2CombineSig(input.MuSig2SessionID, - []*musig2.PartialSignature) (*schnorr.Signature, bool, error) { - - return nil, false, nil -} - -// MuSig2Cleanup removes a session from memory to free up resources. -func (s *SingleSigner) MuSig2Cleanup(input.MuSig2SessionID) error { - return nil -} From ea2505489aa42c86357e4d41ad92139a8d20c682 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 26 Jul 2023 17:03:24 -0700 Subject: [PATCH 11/11] funding: add unit tests for reg+zero conf taproot chans --- funding/manager_test.go | 240 +++++++++++++++++++++++++++++++++++----- 1 file changed, 210 insertions(+), 30 deletions(-) diff --git a/funding/manager_test.go b/funding/manager_test.go index 05ba8e93eae..7566b96b5d0 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -31,6 +31,7 @@ import ( "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest/mock" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -372,9 +373,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, wc := &mock.WalletController{ RootKey: alicePrivKey, } - signer := &mock.SingleSigner{ - Privkey: alicePrivKey, - } + signer := mock.NewSingleSigner(alicePrivKey) bio := &mock.ChainIO{ BestHeight: fundingBroadcastHeight, } @@ -706,12 +705,13 @@ func tearDownFundingManagers(t *testing.T, a, b *testNode) { // transaction is confirmed on-chain. Returns the funding out point. func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, pushAmt btcutil.Amount, numConfs uint32, - updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool) ( + updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool, + chanType *lnwire.ChannelType) ( *wire.OutPoint, *wire.MsgTx) { publ := fundChannel( t, alice, bob, localFundingAmt, pushAmt, false, numConfs, - updateChan, announceChan, nil, + updateChan, announceChan, chanType, ) fundingOutPoint := &wire.OutPoint{ Hash: publ.TxHash(), @@ -720,6 +720,18 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, return fundingOutPoint, publ } +func isTaprootChanType(chanType *lnwire.ChannelType) bool { + if chanType == nil { + return false + } + + featVec := lnwire.RawFeatureVector(*chanType) + + return featVec.IsSet( + lnwire.SimpleTaprootChannelsRequired, + ) +} + // fundChannel takes the funding process to the point where the funding // transaction is confirmed on-chain. Returns the funding tx. func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, @@ -743,6 +755,12 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, Err: errChan, } + // If this is a taproot channel, then we want to force it to be a + // private channel, as that's the only channel type supported for now. + if isTaprootChanType(chanType) { + initReq.Private = true + } + alice.fundingMgr.InitFundingWorkflow(initReq) // Alice should have sent the OpenChannel message to Bob. @@ -767,6 +785,11 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, "alice, instead got %T", aliceMsg) } + // For taproot channel types, the nonce should be set. + if isTaprootChanType(chanType) { + require.NotNil(t, openChannelReq.LocalNonce) + } + // Let Bob handle the init message. bob.fundingMgr.ProcessFundingMsg(openChannelReq, alice) @@ -775,6 +798,11 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, t, bob.msgChan, "AcceptChannel", ).(*lnwire.AcceptChannel) + // For taproot channel types, the nonce should be set. + if isTaprootChanType(chanType) { + require.NotNil(t, acceptChannelResponse.LocalNonce) + } + // They now should both have pending reservations for this channel // active. assertNumPendingReservations(t, alice, bobPubKey, 1) @@ -796,6 +824,11 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, t, alice.msgChan, "FundingCreated", ).(*lnwire.FundingCreated) + // For taproot channel types, the partial signature should be set. + if isTaprootChanType(chanType) { + require.NotNil(t, fundingCreated.PartialSig) + } + // Give the message to Bob. bob.fundingMgr.ProcessFundingMsg(fundingCreated, alice) @@ -804,6 +837,11 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, t, bob.msgChan, "FundingSigned", ).(*lnwire.FundingSigned) + // For taproot channel types, the partial signature should be set. + if isTaprootChanType(chanType) { + require.NotNil(t, fundingSigned.PartialSig) + } + // Forward the signature to Alice. alice.fundingMgr.ProcessFundingMsg(fundingSigned, bob) @@ -1262,6 +1300,25 @@ func assertAnnouncementSignatures(t *testing.T, alice, bob *testNode) { } } +func assertType[T any](t *testing.T, typ any) T { + value, ok := typ.(T) + require.True(t, ok) + + return value +} + +func assertNodeAnnSent(t *testing.T, alice, bob *testNode) { + t.Helper() + + for _, node := range []*testNode{alice, bob} { + nodeAnn, err := lnutils.RecvOrTimeout( + node.msgChan, time.Second*5, + ) + require.NoError(t, err) + assertType[*lnwire.NodeAnnouncement](t, *nodeAnn) + } +} + func waitForOpenUpdate(t *testing.T, updateChan chan *lnrpc.OpenStatusUpdate) { var openUpdate *lnrpc.OpenStatusUpdate select { @@ -1366,14 +1423,31 @@ func assertHandleFundingLocked(t *testing.T, alice, bob *testNode) { } } -func TestFundingManagerNormalWorkflow(t *testing.T) { - t.Parallel() - +func testNormalWorkflow(t *testing.T, chanType *lnwire.ChannelType) { alice, bob := setupFundingManagers(t) t.Cleanup(func() { tearDownFundingManagers(t, alice, bob) }) + // If the channel type is set, then we need to make sure both parties + // support explicit channel type negotiation. + if chanType != nil { + // Alice and Bob will have the same set of feature bits in our + // test. + featureBits := []lnwire.FeatureBit{ + lnwire.ZeroConfOptional, + lnwire.ScidAliasOptional, + lnwire.ExplicitChannelTypeOptional, + lnwire.StaticRemoteKeyOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.SimpleTaprootChannelsOptional, + } + alice.localFeatures = featureBits + alice.remoteFeatures = featureBits + bob.localFeatures = featureBits + bob.remoteFeatures = featureBits + } + // We will consume the channel updates as we go, so no buffering is // needed. updateChan := make(chan *lnrpc.OpenStatusUpdate) @@ -1385,6 +1459,7 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + chanType, ) // Check that neither Alice nor Bob sent an error message. @@ -1415,6 +1490,13 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { t, bob.msgChan, "FundingLocked", ).(*lnwire.FundingLocked) + // For taproot channels, the funding locked messages should have the + // next set of verification nonces. + if isTaprootChanType(chanType) { + require.NotNil(t, fundingLockedAlice.NextLocalNonce) + require.NotNil(t, fundingLockedBob.NextLocalNonce) + } + // Check that the state machine is updated accordingly assertFundingLockedSent(t, alice, bob, fundingOutPoint) @@ -1446,8 +1528,20 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { Tx: fundingTx, } - // Make sure the fundingManagers exchange announcement signatures. - assertAnnouncementSignatures(t, alice, bob) + switch { + // For taproot channels, we expect them to only send a node + // announcement message at this point. These channels aren't advertised + // so we don't expect the other messages. + case isTaprootChanType(chanType): + assertNodeAnnSent(t, alice, bob) + + // For regular channels, we'll make sure the fundingManagers exchange + // announcement signatures. + case chanType == nil: + fallthrough + default: + assertAnnouncementSignatures(t, alice, bob) + } // The internal state-machine should now have deleted the channelStates // from the database, as the channel is announced. @@ -1458,6 +1552,37 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { assertNoFwdingPolicy(t, alice, bob, fundingOutPoint) } +// TestFundingManagerNormalWorkflow tests that the funding manager is able to +// do a routine channel funding flow back and forth. +func TestFundingManagerNormalWorkflow(t *testing.T) { + t.Parallel() + + taprootChanType := lnwire.ChannelType(*lnwire.NewRawFeatureVector( + lnwire.SimpleTaprootChannelsRequired, + )) + + testCases := []struct { + typeName string + chanType *lnwire.ChannelType + }{ + { + typeName: "normal", + chanType: nil, + }, + { + typeName: "taproot", + chanType: &taprootChanType, + }, + } + + //nolint:paralleltest + for _, testCase := range testCases { + t.Run(testCase.typeName, func(t *testing.T) { + testNormalWorkflow(t, testCase.chanType) + }) + } +} + // TestFundingManagerRejectCSV tests checking of local CSV values against our // local CSV limit for incoming and outgoing channels. func TestFundingManagerRejectCSV(t *testing.T) { @@ -1632,6 +1757,7 @@ func TestFundingManagerRestartBehavior(t *testing.T) { updateChan := make(chan *lnrpc.OpenStatusUpdate) fundingOutPoint, fundingTx := openChannel( t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + nil, ) // After the funding transaction gets mined, both nodes will send the @@ -1788,6 +1914,7 @@ func TestFundingManagerOfflinePeer(t *testing.T) { updateChan := make(chan *lnrpc.OpenStatusUpdate) fundingOutPoint, fundingTx := openChannel( t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + nil, ) // After the funding transaction gets mined, both nodes will send the @@ -2171,7 +2298,7 @@ func TestFundingManagerFundingTimeout(t *testing.T) { // Run through the process of opening the channel, up until the funding // transaction is broadcasted. - _, _ = openChannel(t, alice, bob, 500000, 0, 1, updateChan, true) + _, _ = openChannel(t, alice, bob, 500000, 0, 1, updateChan, true, nil) // Bob will at this point be waiting for the funding transaction to be // confirmed, so the channel should be considered pending. @@ -2218,7 +2345,7 @@ func TestFundingManagerFundingNotTimeoutInitiator(t *testing.T) { // Run through the process of opening the channel, up until the funding // transaction is broadcasted. - _, _ = openChannel(t, alice, bob, 500000, 0, 1, updateChan, true) + _, _ = openChannel(t, alice, bob, 500000, 0, 1, updateChan, true, nil) // Alice will at this point be waiting for the funding transaction to be // confirmed, so the channel should be considered pending. @@ -2293,7 +2420,7 @@ func TestFundingManagerReceiveFundingLockedTwice(t *testing.T) { pushAmt := btcutil.Amount(0) capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( - t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + t, alice, bob, localAmt, pushAmt, 1, updateChan, true, nil, ) // Notify that transaction was mined @@ -2404,7 +2531,7 @@ func TestFundingManagerRestartAfterChanAnn(t *testing.T) { pushAmt := btcutil.Amount(0) capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( - t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + t, alice, bob, localAmt, pushAmt, 1, updateChan, true, nil, ) // Notify that transaction was mined @@ -2501,7 +2628,7 @@ func TestFundingManagerRestartAfterReceivingFundingLocked(t *testing.T) { pushAmt := btcutil.Amount(0) capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( - t, alice, bob, localAmt, pushAmt, 1, updateChan, true, + t, alice, bob, localAmt, pushAmt, 1, updateChan, true, nil, ) // Notify that transaction was mined @@ -2594,7 +2721,7 @@ func TestFundingManagerPrivateChannel(t *testing.T) { pushAmt := btcutil.Amount(0) capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( - t, alice, bob, localAmt, pushAmt, 1, updateChan, false, + t, alice, bob, localAmt, pushAmt, 1, updateChan, false, nil, ) // Notify that transaction was mined @@ -2717,7 +2844,7 @@ func TestFundingManagerPrivateRestart(t *testing.T) { pushAmt := btcutil.Amount(0) capacity := localAmt + pushAmt fundingOutPoint, fundingTx := openChannel( - t, alice, bob, localAmt, pushAmt, 1, updateChan, false, + t, alice, bob, localAmt, pushAmt, 1, updateChan, false, nil, ) // Notify that transaction was mined @@ -4150,11 +4277,7 @@ func testUpfrontFailure(t *testing.T, pkscript []byte, expectErr bool) { } } -// TestFundingManagerZeroConf tests that the fundingmanager properly handles -// the whole flow for zero-conf channels. -func TestFundingManagerZeroConf(t *testing.T) { - t.Parallel() - +func testZeroConf(t *testing.T, chanType *lnwire.ChannelType) { alice, bob := setupFundingManagers(t) t.Cleanup(func() { tearDownFundingManagers(t, alice, bob) @@ -4167,6 +4290,7 @@ func TestFundingManagerZeroConf(t *testing.T) { lnwire.ExplicitChannelTypeOptional, lnwire.StaticRemoteKeyOptional, lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.SimpleTaprootChannelsOptional, } alice.localFeatures = featureBits alice.remoteFeatures = featureBits @@ -4178,11 +4302,20 @@ func TestFundingManagerZeroConf(t *testing.T) { updateChan := make(chan *lnrpc.OpenStatusUpdate) // Construct the zero-conf ChannelType for use in open_channel. - channelTypeBits := []lnwire.FeatureBit{ - lnwire.ZeroConfRequired, - lnwire.StaticRemoteKeyRequired, - lnwire.AnchorsZeroFeeHtlcTxRequired, + var channelTypeBits []lnwire.FeatureBit + if isTaprootChanType(chanType) { + channelTypeBits = []lnwire.FeatureBit{ + lnwire.ZeroConfRequired, + lnwire.SimpleTaprootChannelsRequired, + } + } else { + channelTypeBits = []lnwire.FeatureBit{ + lnwire.ZeroConfRequired, + lnwire.StaticRemoteKeyRequired, + lnwire.AnchorsZeroFeeHtlcTxRequired, + } } + channelType := lnwire.ChannelType( *lnwire.NewRawFeatureVector(channelTypeBits...), ) @@ -4253,9 +4386,12 @@ func TestFundingManagerZeroConf(t *testing.T) { Tx: fundingTx, } - assertChannelAnnouncements( - t, alice, bob, fundingAmt, nil, nil, nil, nil, - ) + // For taproot channels, we don't expect them to be announced atm. + if !isTaprootChanType(chanType) { + assertChannelAnnouncements( + t, alice, bob, fundingAmt, nil, nil, nil, nil, + ) + } // Both Alice and Bob should send on reportScidChan. select { @@ -4279,7 +4415,20 @@ func TestFundingManagerZeroConf(t *testing.T) { Tx: fundingTx, } - assertAnnouncementSignatures(t, alice, bob) + switch { + // For taproot channels, we expect them to only send a node + // announcement message at this point. These channels aren't advertised + // so we don't expect the other messages. + case isTaprootChanType(chanType): + assertNodeAnnSent(t, alice, bob) + + // For regular channels, we'll make sure the fundingManagers exchange + // announcement signatures. + case chanType == nil: + fallthrough + default: + assertAnnouncementSignatures(t, alice, bob) + } // Assert that the channel state is deleted from the fundingmanager's // datastore. @@ -4289,3 +4438,34 @@ func TestFundingManagerZeroConf(t *testing.T) { // have been deleted from the database, as the channel is announced. assertNoFwdingPolicy(t, alice, bob, fundingOp) } + +// TestFundingManagerZeroConf tests that the fundingmanager properly handles +// the whole flow for zero-conf channels. +func TestFundingManagerZeroConf(t *testing.T) { + t.Parallel() + + taprootChanType := lnwire.ChannelType(*lnwire.NewRawFeatureVector( + lnwire.SimpleTaprootChannelsRequired, + )) + + testCases := []struct { + typeName string + chanType *lnwire.ChannelType + }{ + { + typeName: "normal", + chanType: nil, + }, + { + typeName: "taproot", + chanType: &taprootChanType, + }, + } + + //nolint:paralleltest + for _, testCase := range testCases { + t.Run(testCase.typeName, func(t *testing.T) { + testZeroConf(t, testCase.chanType) + }) + } +}