diff --git a/funding/aux_funding.go b/funding/aux_funding.go index 6c1c2b9b7..9f041c4e0 100644 --- a/funding/aux_funding.go +++ b/funding/aux_funding.go @@ -2,6 +2,7 @@ package funding import ( "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/protofsm" @@ -17,16 +18,46 @@ type AuxFundingController interface { // handle custom messages specific to the funding type. protofsm.MsgEndpoint - // DescPendingChanID takes a pending channel ID, that may already be + // DescFromPendingChanID takes a pending channel ID, that may already be // known due to prior custom channel messages, and maybe returns an aux // funding desc which can be used to modify how a channel is funded. - // - // TODO(roasbeef): erorr on validation if fail due to invalid root - // match? - DescFromPendingChanID(PendingChanID) fn.Option[lnwallet.AuxFundingDesc] + DescFromPendingChanID(pid PendingChanID, + openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) // DeriveTapscriptRoot takes a pending channel ID and maybe returns a // tapscript root that should be used when creating any musig2 sessions // for a channel. - DeriveTapscriptRoot(PendingChanID) fn.Option[chainhash.Hash] + DeriveTapscriptRoot(PendingChanID) (fn.Option[chainhash.Hash], error) +} + +// descFromPendingChanID takes a pending channel ID, that may already be +// known due to prior custom channel messages, and maybe returns an aux +// funding desc which can be used to modify how a channel is funded. +func descFromPendingChanID(controller fn.Option[AuxFundingController], + chanID PendingChanID, openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) { + + if controller.IsNone() { + return fn.None[lnwallet.AuxFundingDesc](), nil + } + + return controller.UnsafeFromSome().DescFromPendingChanID( + chanID, openChan, localKeyRing, remoteKeyRing, initiator, + ) +} + +// deriveTapscriptRoot takes a pending channel ID and maybe returns a +// tapscript root that should be used when creating any musig2 sessions +// for a channel. +func deriveTapscriptRoot(controller fn.Option[AuxFundingController], + chanID PendingChanID) (fn.Option[chainhash.Hash], error) { + + if controller.IsNone() { + return fn.None[chainhash.Hash](), nil + } + + return controller.UnsafeFromSome().DeriveTapscriptRoot(chanID) } diff --git a/funding/manager.go b/funding/manager.go index 9859ef8d2..64329c04d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -99,7 +99,6 @@ const ( // you and limitless channel size (apart from 21 million cap). MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000) - // TODO(roasbeef): tune. msgBufferSize = 50 // MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait @@ -1622,11 +1621,14 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, // At this point, if we have an AuxFundingController active, we'll // check to see if we have a special tapscript root to use in our // musig2 funding output. - tapscriptRoot := fn.MapOption( - func(a AuxFundingController) fn.Option[chainhash.Hash] { - return a.DeriveTapscriptRoot(msg.PendingChannelID) - }, - )(f.cfg.AuxFundingController) + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, msg.PendingChannelID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + log.Error(err) + f.failFundingFlow(peer, cid, err) + } req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, @@ -1644,7 +1646,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, - TapscriptRoot: fn.FlattenOption(tapscriptRoot), + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -2241,10 +2243,27 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, return } + // At this point, we'll see if there's an AuxFundingDesc we + // need to deliver so the funding process can continue + // properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + failFlow("error continuing PSBT flow", err) + return + } + // A non-nil error means we can continue the funding flow. // Notify the wallet so it can prepare everything we need to // continue. - err = resCtx.reservation.ProcessPsbt() + // + // We'll also pass along the aux funding controller as well, + // which may be used to help process the finalized PSBT. + err = resCtx.reservation.ProcessPsbt(auxFundingDesc) if err != nil { failFlow("error continuing PSBT flow", err) return @@ -2253,8 +2272,8 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, // We are now ready to continue the funding flow. f.continueFundingAccept(resCtx, cid) - // Handle a server shutdown as well because the reservation won't - // survive a restart as it's in memory only. + // Handle a server shutdown as well because the reservation won't + // survive a restart as it's in memory only. case <-f.quit: log.Errorf("Unable to handle funding accept message "+ "for peer_key=%x, pending_chan_id=%x: funding manager "+ @@ -2308,6 +2327,10 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx, // funding flow fails. cid.setChanID(channelID) + // Now that we're ready to resume the funding flow, we'll call into the + // aux controller with the final funding details so we can obtain the + // funding descs we need. + // Send the FundingCreated msg. fundingCreated := &lnwire.FundingCreated{ PendingChannelID: cid.tempChanID, @@ -2370,7 +2393,6 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, // final funding transaction, as well as a signature for our version of // the commitment transaction. So at this point, we can validate the // initiator's commitment transaction, then send our own if it's valid. - // TODO(roasbeef): make case (p vs P) consistent throughout fundingOut := msg.FundingPoint log.Infof("completing pending_id(%x) with ChannelPoint(%v)", pendingChanID[:], fundingOut) @@ -2402,16 +2424,33 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, } } + // At this point, we'll see if there's an AuxFundingDesc we need to + // deliver so the funding process can continue properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + log.Errorf("error continuing PSBT flow: %v", err) + f.failFundingFlow(peer, cid, err) + return + } + // With all the necessary data available, attempt to advance the // funding workflow to the next stage. If this succeeds then the // funding transaction will broadcast after our next message. // CompleteReservationSingle will also mark the channel as 'IsPending' // in the database. + // + // We'll also directly pass in the AuxFundiner controller as well, + // which may be used by the reservation system to finalize funding our + // side. completeChan, err := resCtx.reservation.CompleteReservationSingle( - &fundingOut, commitSig, + &fundingOut, commitSig, auxFundingDesc, ) if err != nil { - // TODO(roasbeef): better error logging: peerID, channelID, etc. log.Errorf("unable to complete single reservation: %v", err) f.failFundingFlow(peer, cid, err) return @@ -2722,9 +2761,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer, // Send an update to the upstream client that the negotiation process // is over. - // - // TODO(roasbeef): add abstraction over updates to accommodate - // long-polling, or SSE, etc. upd := &lnrpc.OpenStatusUpdate{ Update: &lnrpc.OpenStatusUpdate_ChanPending{ ChanPending: &lnrpc.PendingUpdate{ @@ -4429,7 +4465,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, // InitFundingWorkflow sends a message to the funding manager instructing it // to initiate a single funder workflow with the source peer. -// TODO(roasbeef): re-visit blocking nature.. func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) { f.fundingRequests <- msg } @@ -4622,11 +4657,14 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { // At this point, if we have an AuxFundingController active, we'll // check to see if we have a special tapscript root to use in our // musig2 funding output. - tapscriptRoot := fn.MapOption( - func(a AuxFundingController) fn.Option[chainhash.Hash] { - return a.DeriveTapscriptRoot(chanID) - }, - )(f.cfg.AuxFundingController) + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, chanID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + msg.Err <- err + return + } req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, @@ -4651,7 +4689,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, - TapscriptRoot: fn.FlattenOption(tapscriptRoot), + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 1e65f29b3..2dbe93828 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -218,10 +218,14 @@ type ChannelReservation struct { fundingIntent chanfunding.Intent - // initAuxLeaves is an optional set of aux commitment leaves that'll - // modify the way we construct the commitment transaction, in + // localInitAuxLeaves is an optional set of aux commitment leaves + // that'll modify the way we construct the commitment transaction, in // particular the tapscript leaves. - initAuxLeaves fn.Option[CommitAuxLeaves] + localInitAuxLeaves fn.Option[CommitAuxLeaves] + + // remoteInitAuxLeaves is an optional set of aux commitment leaves for + // the remote party. + remoteInitAuxLeaves fn.Option[CommitAuxLeaves] // nextRevocationKeyLoc stores the key locator information for this // channel. @@ -608,12 +612,15 @@ func (r *ChannelReservation) IsCannedShim() bool { } // ProcessPsbt continues a previously paused funding flow that involves PSBT to -// construct the funding transaction. This method can be called once the PSBT is -// finalized and the signed transaction is available. -func (r *ChannelReservation) ProcessPsbt() error { +// construct the funding transaction. This method can be called once the PSBT +// is finalized and the signed transaction is available. +func (r *ChannelReservation) ProcessPsbt( + auxFundingDesc fn.Option[AuxFundingDesc]) error { + errChan := make(chan error, 1) r.wallet.msgChan <- &continueContributionMsg{ + auxFundingDesc: auxFundingDesc, pendingFundingID: r.reservationID, err: errChan, } @@ -715,8 +722,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*input.Sc // available via the .OurSignatures() method. As this method should only be // called as a response to a single funder channel, only a commitment signature // will be populated. -func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoint, - commitSig input.Signature) (*channeldb.OpenChannel, error) { +func (r *ChannelReservation) CompleteReservationSingle( + fundingPoint *wire.OutPoint, commitSig input.Signature, + auxFundingDesc fn.Option[AuxFundingDesc], +) (*channeldb.OpenChannel, error) { errChan := make(chan error, 1) completeChan := make(chan *channeldb.OpenChannel, 1) @@ -726,6 +735,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi fundingOutpoint: fundingPoint, theirCommitmentSig: commitSig, completeChan: completeChan, + auxFundingDesc: auxFundingDesc, err: errChan, } @@ -811,6 +821,36 @@ func (r *ChannelReservation) Cancel() error { return <-errChan } +// ChanState the current open channel state. +func (r *ChannelReservation) ChanState() *channeldb.OpenChannel { + r.RLock() + defer r.RUnlock() + return r.partialState +} + +// CommitmentKeyRings returns the local+remote key ring used for the very first +// commitment transaction both parties. +func (r *ChannelReservation) CommitmentKeyRings() (*CommitmentKeyRing, *CommitmentKeyRing) { + r.RLock() + defer r.RUnlock() + + chanType := r.partialState.ChanType + ourChanCfg := r.ourContribution.ChannelConfig + theirChanCfg := r.theirContribution.ChannelConfig + + localKeys := DeriveCommitmentKeys( + r.ourContribution.FirstCommitmentPoint, true, chanType, + ourChanCfg, theirChanCfg, + ) + + remoteKeys := DeriveCommitmentKeys( + r.theirContribution.FirstCommitmentPoint, false, chanType, + ourChanCfg, theirChanCfg, + ) + + return localKeys, remoteKeys +} + // VerifyConstraints is a helper function that can be used to check the sanity // of various channel constraints. func VerifyConstraints(c *channeldb.ChannelConstraints, diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 401c46683..0a6f20e5f 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -936,6 +937,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, fundingPoint := aliceChanReservation.FundingOutpoint() _, err = bobChanReservation.CompleteReservationSingle( fundingPoint, aliceCommitSig, + fn.None[lnwallet.AuxFundingDesc](), ) require.NoError(t, err, "bob unable to consume single reservation") diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 08abcb423..fd96fae5b 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -108,9 +108,13 @@ type AuxFundingDesc struct { // first commitment entry for the remote party. CustomRemoteCommitBlob tlv.Blob - // InitAuxLeaves is the set of aux leaves that'll be used for the very - // first commitment state. - InitAuxLeaves CommitAuxLeaves + // LocalInitAuxLeaves is the set of aux leaves that'll be used for our + // very first commitment state. + LocalInitAuxLeaves CommitAuxLeaves + + // RemoteInitAuxLeaves is the set of aux leaves that'll be used for the + // very first commitment state for the remote party. + RemoteInitAuxLeaves CommitAuxLeaves } // InitFundingReserveMsg is the first message sent to initiate the workflow @@ -276,6 +280,8 @@ type addContributionMsg struct { type continueContributionMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // NOTE: In order to avoid deadlocks, this channel MUST be buffered. err chan error } @@ -331,6 +337,8 @@ type addCounterPartySigsMsg struct { type addSingleFunderSigsMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // fundingOutpoint is the outpoint of the completed funding // transaction as assembled by the workflow initiator. fundingOutpoint *wire.OutPoint @@ -1474,7 +1482,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // createCommitOpts is a struct that holds the options for creating a a new // commitment transaction. type createCommitOpts struct { - auxLeaves fn.Option[CommitAuxLeaves] + localAuxLeaves fn.Option[CommitAuxLeaves] + remoteAuxLeaves fn.Option[CommitAuxLeaves] } // defaultCommitOpts returns a new createCommitOpts with default values. @@ -1484,9 +1493,12 @@ func defaultCommitOpts() createCommitOpts { // WithAuxLeaves is a functional option that can be used to set the aux leaves // for a new commitment transaction. -func WithAuxLeaves(leaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { +func WithAuxLeaves(localLeaves, + remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { + return func(o *createCommitOpts) { - o.auxLeaves = leaves + o.localAuxLeaves = localLeaves + o.remoteAuxLeaves = remoteLeaves } } @@ -1521,7 +1533,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, theirChanCfg, localBalance, remoteBalance, 0, initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.localAuxLeaves, ) if err != nil { return nil, nil, err @@ -1535,7 +1547,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, ourChanCfg, remoteBalance, localBalance, 0, !initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.remoteAuxLeaves, ) if err != nil { return nil, nil, err @@ -1814,6 +1826,24 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { return } + chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + ourContribution := pendingReservation.ourContribution theirContribution := pendingReservation.theirContribution chanPoint := pendingReservation.partialState.FundingOutpoint @@ -1872,7 +1902,6 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { // Store their current commitment point. We'll need this after the // first state transition in order to verify the authenticity of the // revocation. - chanState := pendingReservation.partialState chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint // Create the txin to our commitment transaction; required to construct @@ -1889,6 +1918,13 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { leaseExpiry = pendingReservation.partialState.ThawHeight } + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, ourContribution.ChannelConfig, theirContribution.ChannelConfig, @@ -1896,7 +1932,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, pendingReservation.partialState.IsInitiator, leaseExpiry, - WithAuxLeaves(pendingReservation.initAuxLeaves), + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2313,6 +2349,23 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { defer pendingReservation.Unlock() chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + chanType := pendingReservation.partialState.ChanType chanState.FundingOutpoint = *req.fundingOutpoint fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) @@ -2326,6 +2379,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { if pendingReservation.partialState.ChanType.HasLeaseExpiration() { leaseExpiry = pendingReservation.partialState.ThawHeight } + + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, pendingReservation.ourContribution.ChannelConfig, @@ -2334,7 +2395,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, - WithAuxLeaves(pendingReservation.initAuxLeaves), + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err