diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 7fd45e1c244..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,29 +286,43 @@ 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() } } - 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 ( + err error ) - if err != nil { - return err - } - pkScript, err := input.WitnessScriptHash(multiSigScript) - if err != nil { - return err + if chanState.ChanType.IsTaproot() { + c.fundingPkScript, _, 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 + } + 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 @@ -567,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 @@ -833,6 +881,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, ) 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/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 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 diff --git a/funding/manager.go b/funding/manager.go index 2d702ba4821..b1bdfa608a3 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 + + if channel.ChanType.IsTaproot() { + pkScript, _, err := input.GenTaprootFundingScript( + localKey, remoteKey, int64(channel.Capacity), + ) + if err != nil { + return nil, err + } - multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey) + 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 @@ -3615,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 @@ -3793,7 +4071,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 +4082,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 +4511,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 +4540,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..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" @@ -233,7 +234,7 @@ func (m *mockZeroConfAcceptor) Accept( } type newChannelMsg struct { - channel *channeldb.OpenChannel + channel *lnpeer.NewChannel err chan error } @@ -297,7 +298,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) @@ -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) + }) + } +} 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) 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/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 -} 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) 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{ 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, } 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,