diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 41a2fcbb7..da1f8e08a 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -68,6 +68,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // BlockCache is the main cache for storing block information. BlockCache *blockcache.BlockCache diff --git a/channeldb/models/channel_edge_info.go b/channeldb/models/channel_edge_info.go index 1afa2d627..0f91e2bbe 100644 --- a/channeldb/models/channel_edge_info.go +++ b/channeldb/models/channel_edge_info.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" ) // ChannelEdgeInfo represents a fully authenticated channel along with all its @@ -62,6 +63,11 @@ type ChannelEdgeInfo struct { // the value output in the outpoint that created this channel. Capacity btcutil.Amount + // TapscriptRoot is the optional Merkle root of the tapscript tree if + // this channel is a taproot channel that also commits to a tapscript + // tree (custom channel). + TapscriptRoot fn.Option[chainhash.Hash] + // ExtraOpaqueData is the set of data that was appended to this // message, some of which we may not actually know how to iterate or // parse. By holding onto this data, we ensure that we're able to diff --git a/config_builder.go b/config_builder.go index 21b1ccee0..ee4064035 100644 --- a/config_builder.go +++ b/config_builder.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -167,6 +168,20 @@ type AuxComponents struct { // MsgRouter is an optional message router that if set will be used in // place of a new blank default message router. MsgRouter fn.Option[msgmux.Router] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom channel types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[funding.AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + + // AuxDataParser is an optional data parser that can be used to parse + // auxiliary data for certain custom channel types. + AuxDataParser fn.Option[AuxDataParser] } // DefaultWalletImpl is the default implementation of our normal, btcwallet @@ -573,6 +588,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, ChanStateDB: dbs.ChanStateDB.ChannelStateDB(), NeutrinoCS: neutrinoCS, AuxLeafStore: aux.AuxLeafStore, + AuxSigner: aux.AuxSigner, ActiveNetParams: d.cfg.ActiveNetParams, FeeURL: d.cfg.FeeURL, Fee: &lncfg.Fee{ @@ -730,6 +746,7 @@ func (d *DefaultWalletImpl) BuildChainControl( NetParams: *walletConfig.NetParams, CoinSelectionStrategy: walletConfig.CoinSelectionStrategy, AuxLeafStore: partialChainControl.Cfg.AuxLeafStore, + AuxSigner: partialChainControl.Cfg.AuxSigner, } // The broadcast is already always active for neutrino nodes, so we @@ -912,10 +929,6 @@ type DatabaseInstances struct { // for native SQL queries for tables that already support it. This may // be nil if the use-native-sql flag was not set. NativeSQLStore *sqldb.BaseDB - - // AuxLeafStore is an optional data source that can be used by custom - // channels to fetch+store various data. - AuxLeafStore fn.Option[lnwallet.AuxLeafStore] } // DefaultDatabaseBuilder is a type that builds the default database backends diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index 5940ee25b..cf575f153 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -2360,9 +2360,12 @@ func createInitChannels(t *testing.T) ( ) bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil) + signerMock := lnwallet.NewDefaultAuxSignerMock(t) alicePool := lnwallet.NewSigPool(1, aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err @@ -2375,6 +2378,8 @@ func createInitChannels(t *testing.T) ( bobPool := lnwallet.NewSigPool(1, bobSigner) channelBob, err := lnwallet.NewLightningChannel( bobSigner, bobChannelState, bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index dbc97939a..d61e47901 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -221,6 +221,10 @@ type ChainArbitratorConfig struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions, a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) chanMachine, err := lnwallet.NewLightningChannel( a.c.cfg.Signer, channel, nil, chanOpts..., @@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // Finally, we'll force close the channel completing // the force close workflow. diff --git a/discovery/gossiper.go b/discovery/gossiper.go index a0d3d0d23..284cc4221 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/graph" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -82,9 +83,10 @@ var ( // can provide that serve useful when processing a specific network // announcement. type optionalMsgFields struct { - capacity *btcutil.Amount - channelPoint *wire.OutPoint - remoteAlias *lnwire.ShortChannelID + capacity *btcutil.Amount + channelPoint *wire.OutPoint + remoteAlias *lnwire.ShortChannelID + tapscriptRoot fn.Option[chainhash.Hash] } // apply applies the optional fields within the functional options. @@ -115,6 +117,14 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField { } } +// TapscriptRoot is an optional field that lets the gossiper know of the root of +// the tapscript tree for a custom channel. +func TapscriptRoot(root fn.Option[chainhash.Hash]) OptionalMsgField { + return func(f *optionalMsgFields) { + f.tapscriptRoot = root + } +} + // RemoteAlias is an optional field that lets the gossiper know that a locally // sent channel update is actually an update for the peer that should replace // the ShortChannelID field with the remote's alias. This is only used for @@ -2598,6 +2608,9 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg, cp := *nMsg.optionalMsgFields.channelPoint edge.ChannelPoint = cp } + + // Optional tapscript root for custom channels. + edge.TapscriptRoot = nMsg.optionalMsgFields.tapscriptRoot } log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64()) diff --git a/funding/aux_funding.go b/funding/aux_funding.go new file mode 100644 index 000000000..492612145 --- /dev/null +++ b/funding/aux_funding.go @@ -0,0 +1,51 @@ +package funding + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/msgmux" +) + +// AuxFundingDescResult is a type alias for a function that returns an optional +// aux funding desc. +type AuxFundingDescResult = fn.Result[fn.Option[lnwallet.AuxFundingDesc]] + +// AuxTapscriptResult is a type alias for a function that returns an optional +// tapscript root. +type AuxTapscriptResult = fn.Result[fn.Option[chainhash.Hash]] + +// AuxFundingController permits the implementation of the funding of custom +// channels types. The controller serves as a MsgEndpoint which allows it to +// intercept custom messages, or even the regular funding messages. The +// controller might also pass along an aux funding desc based on an existing +// pending channel ID. +type AuxFundingController interface { + // Endpoint is the embedded interface that signals that the funding + // controller is also a message endpoint. This'll allow it to handle + // custom messages specific to the funding type. + msgmux.Endpoint + + // 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. + DescFromPendingChanID(pid PendingChanID, openChan lnwallet.AuxChanState, + keyRing lntypes.Dual[lnwallet.CommitmentKeyRing], + initiator bool) AuxFundingDescResult + + // 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) AuxTapscriptResult + + // ChannelReady is called when a channel has been fully opened (multiple + // confirmations) and is ready to be used. This can be used to perform + // any final setup or cleanup. + ChannelReady(openChan lnwallet.AuxChanState) error + + // ChannelFinalized is called when a channel has been fully finalized. + // In this state, we've received the commitment sig from the remote + // party, so we are safe to broadcast the funding transaction. + ChannelFinalized(PendingChanID) error +} diff --git a/funding/manager.go b/funding/manager.go index 3af8ad104..92c75e14b 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 @@ -549,6 +548,16 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom channel types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // Manager acts as an orchestrator/bridge between the wallet's @@ -1078,6 +1087,9 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel, f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // We create the state-machine object which wraps the database state. lnChannel, err := lnwallet.NewLightningChannel( @@ -1256,8 +1268,8 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel, // advancePendingChannelState waits for a pending channel's funding tx to // confirm, and marks it open in the database when that happens. -func (f *Manager) advancePendingChannelState( - channel *channeldb.OpenChannel, pendingChanID PendingChanID) error { +func (f *Manager) advancePendingChannelState(channel *channeldb.OpenChannel, + pendingChanID PendingChanID) error { if channel.IsZeroConf() { // Persist the alias to the alias database. @@ -1626,6 +1638,23 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, return } + // 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 + // MuSig funding output. + tapscriptRoot, err := fn.MapOptionZ( + f.cfg.AuxFundingController, + func(c AuxFundingController) AuxTapscriptResult { + return c.DeriveTapscriptRoot(msg.PendingChannelID) + }, + ).Unpack() + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + log.Error(err) + f.failFundingFlow(peer, cid, err) + + return + } + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: msg.PendingChannelID, @@ -1642,6 +1671,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -1898,6 +1928,8 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, log.Debugf("Remote party accepted commitment rendering params: %v", lnutils.SpewLogClosure(params)) + reservation.SetState(lnwallet.SentAcceptChannel) + // With the initiator's contribution recorded, respond with our // contribution in the next message of the workflow. fundingAccept := lnwire.AcceptChannel{ @@ -1958,6 +1990,10 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer, // Update the timestamp once the fundingAcceptMsg has been handled. defer resCtx.updateTimestamp() + if resCtx.reservation.State() != lnwallet.SentOpenChannel { + return + } + log.Infof("Recv'd fundingResponse for pending_id(%x)", pendingChanID[:]) @@ -2261,10 +2297,34 @@ 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. + auxFundingDesc, err := fn.MapOptionZ( + f.cfg.AuxFundingController, + func(c AuxFundingController) AuxFundingDescResult { + return c.DescFromPendingChanID( + cid.tempChanID, + lnwallet.NewAuxChanState( + resCtx.reservation.ChanState(), + ), + resCtx.reservation.CommitmentKeyRings(), + true, + ) + }, + ).Unpack() + 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 @@ -2359,6 +2419,8 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx, } } + resCtx.reservation.SetState(lnwallet.SentFundingCreated) + if err := resCtx.peer.SendMessage(true, fundingCreated); err != nil { log.Errorf("Unable to send funding complete message: %v", err) f.failFundingFlow(resCtx.peer, cid, err) @@ -2390,11 +2452,14 @@ 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) + if resCtx.reservation.State() != lnwallet.SentAcceptChannel { + return + } + // Create the channel identifier without setting the active channel ID. cid := newChanIdentifier(pendingChanID) @@ -2422,16 +2487,38 @@ 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. + auxFundingDesc, err := fn.MapOptionZ( + f.cfg.AuxFundingController, + func(c AuxFundingController) AuxFundingDescResult { + return c.DescFromPendingChanID( + cid.tempChanID, lnwallet.NewAuxChanState( + resCtx.reservation.ChanState(), + ), resCtx.reservation.CommitmentKeyRings(), + true, + ) + }, + ).Unpack() + 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 AuxFunding 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 @@ -2632,6 +2719,14 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer, return } + if resCtx.reservation.State() != lnwallet.SentFundingCreated { + err := fmt.Errorf("unable to find reservation for chan_id=%x", + msg.ChanID) + f.failFundingFlow(peer, cid, err) + + return + } + // Create an entry in the local discovery map so we can ensure that we // process the channel confirmation fully before we receive a // channel_ready message. @@ -2727,6 +2822,21 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer, } } + // Before we proceed, if we have a funding hook that wants a + // notification that it's safe to broadcast the funding transaction, + // then we'll send that now. + err = fn.MapOptionZ( + f.cfg.AuxFundingController, + func(controller AuxFundingController) error { + return controller.ChannelFinalized(cid.tempChanID) + }, + ) + if err != nil { + log.Errorf("Failed to inform aux funding controller about "+ + "ChannelPoint(%v) being finalized: %v", fundingPoint, + err) + } + // Now that we have a finalized reservation for this funding flow, // we'll send the to be active channel to the ChainArbitrator so it can // watch for any on-chain actions before the channel has fully @@ -2742,9 +2852,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{ @@ -3450,6 +3557,7 @@ func (f *Manager) addToGraph(completeChan *channeldb.OpenChannel, errChan := f.cfg.SendAnnouncement( ann.chanAnn, discovery.ChannelCapacity(completeChan.Capacity), discovery.ChannelPoint(completeChan.FundingOutpoint), + discovery.TapscriptRoot(completeChan.TapscriptRoot), ) select { case err := <-errChan: @@ -3646,7 +3754,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel, // waitForZeroConfChannel is called when the state is addedToGraph with // a zero-conf channel. This will wait for the real confirmation, add the -// confirmed SCID to the graph, and then announce after six confs. +// confirmed SCID to the router graph, and then announce after six confs. func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error { // First we'll check whether the channel is confirmed on-chain. If it // is already confirmed, the chainntnfs subsystem will return with the @@ -3977,6 +4085,26 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen PubNonce: remoteNonce, }), ) + + // Inform the aux funding controller that the liquidity in the + // custom channel is now ready to be advertised. We potentially + // haven't sent our own channel ready message yet, but other + // than that the channel is ready to count toward available + // liquidity. + err = fn.MapOptionZ( + f.cfg.AuxFundingController, + func(controller AuxFundingController) error { + return controller.ChannelReady( + lnwallet.NewAuxChanState(channel), + ) + }, + ) + if err != nil { + cid := newChanIdentifier(msg.ChanID) + f.sendWarning(peer, cid, err) + + return + } } // The channel_ready message contains the next commitment point we'll @@ -4063,6 +4191,19 @@ func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel, log.Debugf("Channel(%v) with ShortChanID %v: successfully "+ "added to graph", chanID, scid) + err = fn.MapOptionZ( + f.cfg.AuxFundingController, + func(controller AuxFundingController) error { + return controller.ChannelReady( + lnwallet.NewAuxChanState(channel), + ) + }, + ) + if err != nil { + return fmt.Errorf("failed notifying aux funding controller "+ + "about channel ready: %w", err) + } + // Give the caller a final update notifying them that the channel is fundingPoint := channel.FundingOutpoint cp := &lnrpc.ChannelPoint{ @@ -4376,9 +4517,9 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, // // We can pass in zeroes for the min and max htlc policy, because we // only use the channel announcement message from the returned struct. - ann, err := f.newChanAnnouncement(localIDKey, remoteIDKey, - localFundingKey, remoteFundingKey, shortChanID, chanID, - 0, 0, nil, chanType, + ann, err := f.newChanAnnouncement( + localIDKey, remoteIDKey, localFundingKey, remoteFundingKey, + shortChanID, chanID, 0, 0, nil, chanType, ) if err != nil { log.Errorf("can't generate channel announcement: %v", err) @@ -4444,7 +4585,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 } @@ -4634,6 +4774,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { scidFeatureVal = true } + // 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, err := fn.MapOptionZ( + f.cfg.AuxFundingController, + func(c AuxFundingController) AuxTapscriptResult { + return c.DeriveTapscriptRoot(chanID) + }, + ).Unpack() + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + log.Error(err) + msg.Err <- err + + return + } + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: chanID, @@ -4673,6 +4830,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -4824,6 +4982,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { log.Infof("Starting funding workflow with %v for pending_id(%x), "+ "committype=%v", msg.Peer.Address(), chanID, commitType) + reservation.SetState(lnwallet.SentOpenChannel) + fundingOpen := lnwire.OpenChannel{ ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash, PendingChannelID: chanID, diff --git a/funding/manager_test.go b/funding/manager_test.go index dce8475ad..ca598cec7 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -567,6 +567,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, AuxLeafStore: fn.Some[lnwallet.AuxLeafStore]( &lnwallet.MockAuxLeafStore{}, ), + AuxSigner: fn.Some[lnwallet.AuxSigner]( + &lnwallet.MockAuxSigner{}, + ), } for _, op := range options { @@ -677,6 +680,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { DeleteAliasEdge: oldCfg.DeleteAliasEdge, AliasManager: oldCfg.AliasManager, AuxLeafStore: oldCfg.AuxLeafStore, + AuxSigner: oldCfg.AuxSigner, }) require.NoError(t, err, "failed recreating aliceFundingManager") diff --git a/graph/builder.go b/graph/builder.go index 8b8b40d64..6930f1a89 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -1093,8 +1093,8 @@ func (b *Builder) addZombieEdge(chanID uint64) error { // segwit v1 (taproot) channels. // // TODO(roasbeef: export and use elsewhere? -func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, - chanFeatures []byte) ([]byte, error) { +func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, chanFeatures []byte, + tapscriptRoot fn.Option[chainhash.Hash]) ([]byte, error) { legacyFundingScript := func() ([]byte, error) { witnessScript, err := input.GenMultiSigScript( @@ -1141,7 +1141,7 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, } fundingScript, _, err := input.GenTaprootFundingScript( - pubKey1, pubKey2, 0, fn.None[chainhash.Hash](), + pubKey1, pubKey2, 0, tapscriptRoot, ) if err != nil { return nil, err @@ -1275,7 +1275,7 @@ func (b *Builder) processUpdate(msg interface{}, // reality. fundingPkScript, err := makeFundingScript( msg.BitcoinKey1Bytes[:], msg.BitcoinKey2Bytes[:], - msg.Features, + msg.Features, msg.TapscriptRoot, ) if err != nil { return err diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 5e5bc8dc2..928f35d44 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2192,10 +2192,20 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // We just received a new updates to our local commitment // chain, validate this new commitment, closing the link if // invalid. + auxSigBlob, err := msg.CustomRecords.Serialize() + if err != nil { + l.failf( + LinkFailureError{code: ErrInvalidCommitment}, + "unable to serialize custom records: %v", err, + ) + + return + } err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{ CommitSig: msg.CommitSig, HtlcSigs: msg.HtlcSigs, PartialSig: msg.PartialSig, + AuxSigBlob: auxSigBlob, }) if err != nil { // If we were unable to reconstruct their proposed @@ -2622,11 +2632,17 @@ func (l *channelLink) updateCommitTx() error { default: } + auxBlobRecords, err := lnwire.ParseCustomRecords(newCommit.AuxSigBlob) + if err != nil { + return fmt.Errorf("error parsing aux sigs: %w", err) + } + commitSig := &lnwire.CommitSig{ - ChanID: l.ChanID(), - CommitSig: newCommit.CommitSig, - HtlcSigs: newCommit.HtlcSigs, - PartialSig: newCommit.PartialSig, + ChanID: l.ChanID(), + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, + PartialSig: newCommit.PartialSig, + CustomRecords: auxBlobRecords, } l.cfg.Peer.SendMessage(false, commitSig) @@ -3778,7 +3794,18 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC, // As we're the exit hop, we'll double check the hop-payload included in // the HTLC to ensure that it was crafted correctly by the sender and // is compatible with the HTLC we were extended. - if add.Amount < fwdInfo.AmountToForward { + // + // For a special case, if the fwdInfo doesn't have any blinded path + // information, and the incoming HTLC had special extra data, then + // we'll skip this amount check. The invoice acceptor will make sure we + // reject the HTLC if it's not containing the correct amount after + // examining the custom data. + hasBlindedPath := fwdInfo.NextBlinding.IsSome() + customHTLC := len(add.CustomRecords) > 0 && !hasBlindedPath + log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v", + hasBlindedPath, customHTLC) + + if !customHTLC && add.Amount < fwdInfo.AmountToForward { l.log.Errorf("onion payload of incoming htlc(%x) has "+ "incompatible value: expected <=%v, got %v", add.PaymentHash, add.Amount, fwdInfo.AmountToForward) diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index aa5fd6e4e..018601a04 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -268,9 +268,12 @@ func TestChannelLinkRevThenSig(t *testing.T) { // Restart Bob as well by calling NewLightningChannel. bobSigner := harness.bobChannel.Signer + signerMock := lnwallet.NewDefaultAuxSignerMock(t) bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner) bobChannel, err := lnwallet.NewLightningChannel( bobSigner, harness.bobChannel.State(), bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) require.NoError(t, err) err = bobPool.Start() @@ -403,9 +406,12 @@ func TestChannelLinkSigThenRev(t *testing.T) { // Restart Bob as well by calling NewLightningChannel. bobSigner := harness.bobChannel.Signer + signerMock := lnwallet.NewDefaultAuxSignerMock(t) bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner) bobChannel, err := lnwallet.NewLightningChannel( bobSigner, harness.bobChannel.State(), bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) require.NoError(t, err) err = bobPool.Start() diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index abd48e806..450d5a19d 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -353,8 +353,11 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, ) alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner) + signerMock := lnwallet.NewDefaultAuxSignerMock(t) channelAlice, err := lnwallet.NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err @@ -364,6 +367,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner) channelBob, err := lnwallet.NewLightningChannel( bobSigner, bobChannelState, bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err @@ -425,6 +430,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, newAliceChannel, err := lnwallet.NewLightningChannel( aliceSigner, aliceStoredChannel, alicePool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, errors.Errorf("unable to create new channel: %v", @@ -471,6 +478,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, newBobChannel, err := lnwallet.NewLightningChannel( bobSigner, bobStoredChannel, bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, errors.Errorf("unable to create new channel: %v", diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index ac39bb497..bedc61722 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -177,6 +177,17 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, }, ) + // If this is a taproot channel, then we'll decode the PSBT to assert + // that an internal key is included. + if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + decodedPSBT, err := psbt.NewFromRawBytes( + bytes.NewReader(tempPsbt), false, + ) + require.NoError(ht, err) + + require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32) + } + // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 8ff18e4b1..26ab31dd9 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -4703,6 +4703,8 @@ type Channel struct { // useful information. This is only ever stored locally and in no way impacts // the channel's operation. Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"` + // Custom channel data that might be populated in custom channels. + CustomChannelData []byte `protobuf:"bytes,37,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *Channel) Reset() { @@ -4993,6 +4995,13 @@ func (x *Channel) GetMemo() string { return "" } +func (x *Channel) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type ListChannelsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -9505,6 +9514,9 @@ type ChannelBalanceResponse struct { PendingOpenLocalBalance *Amount `protobuf:"bytes,7,opt,name=pending_open_local_balance,json=pendingOpenLocalBalance,proto3" json:"pending_open_local_balance,omitempty"` // Sum of channels pending remote balances. PendingOpenRemoteBalance *Amount `protobuf:"bytes,8,opt,name=pending_open_remote_balance,json=pendingOpenRemoteBalance,proto3" json:"pending_open_remote_balance,omitempty"` + // Custom channel data that might be populated if there are custom channels + // present. + CustomChannelData []byte `protobuf:"bytes,9,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *ChannelBalanceResponse) Reset() { @@ -9597,6 +9609,13 @@ func (x *ChannelBalanceResponse) GetPendingOpenRemoteBalance() *Amount { return nil } +func (x *ChannelBalanceResponse) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type QueryRoutesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -17642,6 +17661,8 @@ type PendingChannelsResponse_PendingChannel struct { // useful information. This is only ever stored locally and in no way // impacts the channel's operation. Memo string `protobuf:"bytes,13,opt,name=memo,proto3" json:"memo,omitempty"` + // Custom channel data that might be populated in custom channels. + CustomChannelData []byte `protobuf:"bytes,34,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *PendingChannelsResponse_PendingChannel) Reset() { @@ -17767,6 +17788,13 @@ func (x *PendingChannelsResponse_PendingChannel) GetMemo() string { return "" } +func (x *PendingChannelsResponse_PendingChannel) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type PendingChannelsResponse_PendingOpenChannel struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -18652,7 +18680,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x61, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73, - 0x22, 0xad, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, + 0x22, 0xdd, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6d, @@ -18743,6 +18771,9 @@ var file_lightning_proto_rawDesc = []byte{ 0x69, 0x61, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x53, 0x63, 0x69, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, + 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, @@ -19326,7 +19357,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x61, 0x77, 0x5f, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x61, - 0x77, 0x54, 0x78, 0x22, 0xe1, 0x13, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, + 0x77, 0x54, 0x78, 0x22, 0x91, 0x14, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x62, 0x6f, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x74, 0x6f, @@ -19358,7 +19389,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x14, 0x77, 0x61, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xb3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xe3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x12, @@ -19393,7 +19424,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x47, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, @@ -19571,7 +19605,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x04, 0x52, 0x03, 0x73, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x80, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, + 0x65, 0x73, 0x74, 0x22, 0xb0, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x14, @@ -19603,7 +19637,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x18, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20, diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 6d975c8c3..b44b04caa 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -1592,6 +1592,11 @@ message Channel { the channel's operation. */ string memo = 36; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 37; } message ListChannelsRequest { @@ -2709,6 +2714,11 @@ message PendingChannelsResponse { impacts the channel's operation. */ string memo = 13; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 34; } message PendingOpenChannel { @@ -2968,6 +2978,12 @@ message ChannelBalanceResponse { // Sum of channels pending remote balances. Amount pending_open_remote_balance = 8; + + /* + Custom channel data that might be populated if there are custom channels + present. + */ + bytes custom_channel_data = 9; } message QueryRoutesRequest { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index fe204e12c..0e69fe1fd 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -3127,6 +3127,11 @@ "memo": { "type": "string", "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way\nimpacts the channel's operation." + }, + "custom_channel_data": { + "type": "string", + "format": "byte", + "description": "Custom channel data that might be populated in custom channels." } } }, @@ -3849,6 +3854,11 @@ "memo": { "type": "string", "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation." + }, + "custom_channel_data": { + "type": "string", + "format": "byte", + "description": "Custom channel data that might be populated in custom channels." } } }, @@ -4052,6 +4062,11 @@ "pending_open_remote_balance": { "$ref": "#/definitions/lnrpcAmount", "description": "Sum of channels pending remote balances." + }, + "custom_channel_data": { + "type": "string", + "format": "byte", + "description": "Custom channel data that might be populated if there are custom channels\npresent." } } }, diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 1a428e5bd..e58ed4d27 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -25,6 +25,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/subscribe" "github.com/lightningnetwork/lnd/zpay32" + "google.golang.org/protobuf/proto" ) const ( @@ -104,6 +105,10 @@ type RouterBackend struct { // TODO(yy): remove this config after the new status code is fully // deployed to the network(v0.20.0). UseStatusInitiated bool + + // ParseCustomChannelData is a function that can be used to parse custom + // channel data from the first hop of a route. + ParseCustomChannelData func(message proto.Message) error } // MissionControl defines the mission control dependencies of routerrpc. @@ -596,8 +601,14 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error) resp.CustomChannelData = customData - // TODO(guggero): Feed the route into the custom data parser - // (part 3 of the mega PR series). + // Allow the aux data parser to parse the custom records into + // a human-readable JSON (if available). + if r.ParseCustomChannelData != nil { + err := r.ParseCustomChannelData(resp) + if err != nil { + return nil, err + } + } } incomingAmt := route.TotalAmount diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 9538c48e7..add5b91f9 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -964,6 +964,11 @@ func (h *HarnessTest) AssertChannelBalanceResp(hn *node.HarnessNode, expected *lnrpc.ChannelBalanceResponse) { resp := hn.RPC.ChannelBalance() + + // Ignore custom channel data of both expected and actual responses. + expected.CustomChannelData = nil + resp.CustomChannelData = nil + require.True(h, proto.Equal(expected, resp), "balance is incorrect "+ "got: %v, want: %v", resp, expected) } diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go new file mode 100644 index 000000000..5d4bc7924 --- /dev/null +++ b/lnwallet/aux_signer.go @@ -0,0 +1,250 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// htlcCustomSigType is the TLV type that is used to encode the custom HTLC +// signatures within the custom data for an existing HTLC. +var htlcCustomSigType tlv.TlvType65543 + +// AuxHtlcDescriptor is a struct that contains the information needed to sign or +// verify an HTLC for custom channels. +type AuxHtlcDescriptor struct { + // ChanID is the ChannelID of the LightningChannel that this + // paymentDescriptor belongs to. We track this here so we can + // reconstruct the Messages that this paymentDescriptor is built from. + ChanID lnwire.ChannelID + + // RHash is the payment hash for this HTLC. The HTLC can be settled iff + // the preimage to this hash is presented. + RHash PaymentHash + + // Timeout is the absolute timeout in blocks, after which this HTLC + // expires. + Timeout uint32 + + // Amount is the HTLC amount in milli-satoshis. + Amount lnwire.MilliSatoshi + + // HtlcIndex is the index within the main update log for this HTLC. + // Entries within the log of type Add will have this field populated, + // as other entries will point to the entry via this counter. + // + // NOTE: This field will only be populated if EntryType is Add. + HtlcIndex uint64 + + // ParentIndex is the HTLC index of the entry that this update settles + // or times out. + // + // NOTE: This field will only be populated if EntryType is Fail or + // Settle. + ParentIndex uint64 + + // EntryType denotes the exact type of the paymentDescriptor. In the + // case of a Timeout, or Settle type, then the Parent field will point + // into the log to the HTLC being modified. + EntryType updateType + + // CustomRecords also stores the set of optional custom records that + // may have been attached to a sent HTLC. + CustomRecords lnwire.CustomRecords + + // addCommitHeight[Remote|Local] encodes the height of the commitment + // which included this HTLC on either the remote or local commitment + // chain. This value is used to determine when an HTLC is fully + // "locked-in". + addCommitHeightRemote uint64 + addCommitHeightLocal uint64 + + // removeCommitHeight[Remote|Local] encodes the height of the + // commitment which removed the parent pointer of this + // paymentDescriptor either due to a timeout or a settle. Once both + // these heights are below the tail of both chains, the log entries can + // safely be removed. + removeCommitHeightRemote uint64 + removeCommitHeightLocal uint64 +} + +// AddHeight returns the height at which the HTLC was added to the commitment +// chain. The height is returned based on the chain the HTLC is being added to +// (local or remote chain). +func (a *AuxHtlcDescriptor) AddHeight( + whoseCommitChain lntypes.ChannelParty) uint64 { + + if whoseCommitChain.IsRemote() { + return a.addCommitHeightRemote + } + + return a.addCommitHeightLocal +} + +// RemoveHeight returns the height at which the HTLC was removed from the +// commitment chain. The height is returned based on the chain the HTLC is being +// removed from (local or remote chain). +func (a *AuxHtlcDescriptor) RemoveHeight( + whoseCommitChain lntypes.ChannelParty) uint64 { + + if whoseCommitChain.IsRemote() { + return a.removeCommitHeightRemote + } + + return a.removeCommitHeightLocal +} + +// newAuxHtlcDescriptor creates a new AuxHtlcDescriptor from a payment +// descriptor. +func newAuxHtlcDescriptor(p *paymentDescriptor) AuxHtlcDescriptor { + return AuxHtlcDescriptor{ + ChanID: p.ChanID, + RHash: p.RHash, + Timeout: p.Timeout, + Amount: p.Amount, + HtlcIndex: p.HtlcIndex, + ParentIndex: p.ParentIndex, + EntryType: p.EntryType, + CustomRecords: p.CustomRecords.Copy(), + addCommitHeightRemote: p.addCommitHeightRemote, + addCommitHeightLocal: p.addCommitHeightLocal, + removeCommitHeightRemote: p.removeCommitHeightRemote, + removeCommitHeightLocal: p.removeCommitHeightLocal, + } +} + +// BaseAuxJob is a struct that contains the common fields that are shared among +// the aux sign/verify jobs. +type BaseAuxJob struct { + // OutputIndex is the output index of the HTLC on the commitment + // transaction being signed. + // + // NOTE: If the output is dust from the PoV of the commitment chain, + // then this value will be -1. + OutputIndex int32 + + // KeyRing is the commitment key ring that contains the keys needed to + // generate the second level HTLC signatures. + KeyRing CommitmentKeyRing + + // HTLC is the HTLC that is being signed or verified. + HTLC AuxHtlcDescriptor + + // Incoming is a boolean that indicates if the HTLC is incoming or + // outgoing. + Incoming bool + + // CommitBlob is the commitment transaction blob that contains the aux + // information for this channel. + CommitBlob fn.Option[tlv.Blob] + + // HtlcLeaf is the aux tap leaf that corresponds to the HTLC being + // signed/verified. + HtlcLeaf input.AuxTapLeaf +} + +// AuxSigJob is a struct that contains all the information needed to sign an +// HTLC for custom channels. +type AuxSigJob struct { + // SignDesc is the sign desc for this HTLC. + SignDesc input.SignDescriptor + + BaseAuxJob + + // Resp is a channel that will be used to send the result of the sign + // job. This channel MUST be buffered. + Resp chan AuxSigJobResp + + // Cancel is a channel that is closed by the caller if they wish to + // abandon all pending sign jobs part of a single batch. This should + // never be closed by the validator. + Cancel <-chan struct{} +} + +// NewAuxSigJob creates a new AuxSigJob. +func NewAuxSigJob(sigJob SignJob, keyRing CommitmentKeyRing, incoming bool, + htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf, cancelChan <-chan struct{}) AuxSigJob { + + return AuxSigJob{ + SignDesc: sigJob.SignDesc, + BaseAuxJob: BaseAuxJob{ + OutputIndex: sigJob.OutputIndex, + KeyRing: keyRing, + HTLC: htlc, + Incoming: incoming, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + Resp: make(chan AuxSigJobResp, 1), + Cancel: cancelChan, + } +} + +// AuxSigJobResp is a struct that contains the result of a sign job. +type AuxSigJobResp struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opaque TLV field that may contain the signature and other data. + SigBlob fn.Option[tlv.Blob] + + // HtlcIndex is the index of the HTLC that was signed. + HtlcIndex uint64 + + // Err is the error that occurred when executing the specified + // signature job. In the case that no error occurred, this value will + // be nil. + Err error +} + +// AuxVerifyJob is a struct that contains all the information needed to verify +// an HTLC for custom channels. +type AuxVerifyJob struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opaque TLV field that may contain the signature and other data. + SigBlob fn.Option[tlv.Blob] + + BaseAuxJob +} + +// NewAuxVerifyJob creates a new AuxVerifyJob. +func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing, + incoming bool, htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf) AuxVerifyJob { + + return AuxVerifyJob{ + SigBlob: sig, + BaseAuxJob: BaseAuxJob{ + KeyRing: keyRing, + HTLC: htlc, + Incoming: incoming, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + } +} + +// AuxSigner is an interface that is used to sign and verify HTLCs for custom +// channels. It is similar to the existing SigPool, but uses opaque blobs to +// shuffle around signature information and other metadata. +type AuxSigner interface { + // SubmitSecondLevelSigBatch takes a batch of aux sign jobs and + // processes them asynchronously. + SubmitSecondLevelSigBatch(chanState AuxChanState, commitTx *wire.MsgTx, + sigJob []AuxSigJob) error + + // PackSigs takes a series of aux signatures and packs them into a + // single blob that can be sent alongside the CommitSig messages. + PackSigs([]fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]] + + // UnpackSigs takes a packed blob of signatures and returns the + // original signatures for each HTLC, keyed by HTLC index. + UnpackSigs(fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]] + + // VerifySecondLevelSigs attempts to synchronously verify a batch of aux + // sig jobs. + VerifySecondLevelSigs(chanState AuxChanState, commitTx *wire.MsgTx, + verifyJob []AuxVerifyJob) error +} diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 177cc35c3..b3457f21b 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -98,6 +99,26 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { ) } +// TaprootInternalKey may return the internal key for a MuSig2 funding output, +// but only if this is actually a MuSig2 channel. +func (s *ShimIntent) TaprootInternalKey() fn.Option[*btcec.PublicKey] { + if !s.musig2 { + return fn.None[*btcec.PublicKey]() + } + + // Similar to the existing p2wsh script, we'll always ensure the keys + // are sorted before use. Since we're only interested in the internal + // key, we don't need to take into account any tapscript root. + // + // We ignore the error here as this is only called after FundingOutput + // is called. + combinedKey, _, _, _ := musig2.AggregateKeys( + []*btcec.PublicKey{s.localKey.PubKey, s.remoteKey}, true, + ) + + return fn.Some(combinedKey.PreTweakedKey) +} + // Cancel allows the caller to cancel a funding Intent at any time. This will // return any resources such as coins back to the eligible pool to be used in // order channel fundings. diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 10bcd7015..f678f520f 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -6,11 +6,14 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) @@ -162,6 +165,13 @@ func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor, i.State = PsbtOutputKnown } +// BindTapscriptRoot takes an optional tapscript root and binds it to the +// underlying funding intent. This only applies to musig2 channels, and will be +// used to make the musig2 funding output. +func (i *PsbtIntent) BindTapscriptRoot(root fn.Option[chainhash.Hash]) { + i.tapscriptRoot = root +} + // FundingParams returns the parameters that are necessary to start funding the // channel output this intent was created for. It returns the P2WSH funding // address, the exact funding amount and a PSBT packet that contains exactly one @@ -208,7 +218,18 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet, } } packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out) - packet.Outputs = append(packet.Outputs, psbt.POutput{}) + + var pOut psbt.POutput + + // If this is a MuSig2 channel, we also need to communicate the internal + // key to the caller. Otherwise, they cannot verify the construction of + // the P2TR output script. + pOut.TaprootInternalKey = fn.MapOptionZ( + i.TaprootInternalKey(), schnorr.SerializePubKey, + ) + + packet.Outputs = append(packet.Outputs, pOut) + return addr, out.Value, packet, nil } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 32492171f..c78742d13 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "math" - "sort" + "slices" "sync" "github.com/btcsuite/btcd/blockchain" @@ -760,6 +760,10 @@ type LightningChannel struct { // signatures, of which there may be hundreds. sigPool *SigPool + // auxSigner is a special signer used to obtain opaque signatures for + // custom channel variants. + auxSigner fn.Option[AuxSigner] + // Capacity is the total capacity of this channel. Capacity btcutil.Amount @@ -821,6 +825,7 @@ type channelOpts struct { remoteNonce *musig2.Nonces leafStore fn.Option[AuxLeafStore] + auxSigner fn.Option[AuxSigner] skipNonceInit bool } @@ -859,6 +864,13 @@ func WithLeafStore(store AuxLeafStore) ChannelOpt { } } +// WithAuxSigner is used to specify a custom aux signer for the channel. +func WithAuxSigner(signer AuxSigner) ChannelOpt { + return func(o *channelOpts) { + o.auxSigner = fn.Some[AuxSigner](signer) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -911,6 +923,7 @@ func NewLightningChannel(signer input.Signer, lc := &LightningChannel{ Signer: signer, leafStore: opts.leafStore, + auxSigner: opts.auxSigner, sigPool: sigPool, currentHeight: localCommit.CommitHeight, commitChains: commitChains, @@ -2545,6 +2558,18 @@ type HtlcView struct { FeePerKw chainfee.SatPerKWeight } +// AuxOurUpdates returns the outgoing HTLCs as a read-only copy of +// AuxHtlcDescriptors. +func (v *HtlcView) AuxOurUpdates() []AuxHtlcDescriptor { + return fn.Map(newAuxHtlcDescriptor, v.OurUpdates) +} + +// AuxTheirUpdates returns the incoming HTLCs as a read-only copy of +// AuxHtlcDescriptors. +func (v *HtlcView) AuxTheirUpdates() []AuxHtlcDescriptor { + return fn.Map(newAuxHtlcDescriptor, v.TheirUpdates) +} + // fetchHTLCView returns all the candidate HTLC updates which should be // considered for inclusion within a commitment based on the passed HTLC log // indexes. @@ -3064,7 +3089,8 @@ func processFeeUpdate(feeUpdate *paymentDescriptor, nextHeight uint64, func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanState *channeldb.OpenChannel, leaseExpiry uint32, remoteCommitView *commitment, - leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) { + leafStore fn.Option[AuxLeafStore]) ([]SignJob, []AuxSigJob, + chan struct{}, error) { var ( isRemoteInitiator = !chanState.IsInitiator @@ -3084,6 +3110,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, numSigs := len(remoteCommitView.incomingHTLCs) + len(remoteCommitView.outgoingHTLCs) sigBatch := make([]SignJob, 0, numSigs) + auxSigBatch := make([]AuxSigJob, 0, numSigs) var err error cancelChan := make(chan struct{}) @@ -3098,8 +3125,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, }, ).Unpack() if err != nil { - return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w", - err) + return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ + "%w", err) } // For each outgoing and incoming HTLC, if the HTLC isn't considered a @@ -3148,12 +3175,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, auxLeaf, ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - // TODO(roasbeef): hook up signer interface here (later commit - // in this PR). - // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3185,6 +3209,11 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, } sigBatch = append(sigBatch, sigJob) + + auxSigBatch = append(auxSigBatch, NewAuxSigJob( + sigJob, *keyRing, true, newAuxHtlcDescriptor(&htlc), + remoteCommitView.customBlob, auxLeaf, cancelChan, + )) } for _, htlc := range remoteCommitView.outgoingHTLCs { if HtlcIsDust( @@ -3228,7 +3257,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, auxLeaf, ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // Construct a full hash cache as we may be signing a segwit v1 @@ -3257,13 +3286,19 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // If this is a taproot channel, then we'll need to set the // method type to ensure we generate a valid signature. if chanType.IsTaproot() { - sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll + //nolint:lll + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod } sigBatch = append(sigBatch, sigJob) + + auxSigBatch = append(auxSigBatch, NewAuxSigJob( + sigJob, *keyRing, false, newAuxHtlcDescriptor(&htlc), + remoteCommitView.customBlob, auxLeaf, cancelChan, + )) } - return sigBatch, cancelChan, nil + return sigBatch, auxSigBatch, cancelChan, nil } // createCommitDiff will create a commit diff given a new pending commitment @@ -3272,7 +3307,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // new commitment to the remote party. The commit diff returned contains all // information necessary for retransmission. func (lc *LightningChannel) createCommitDiff(newCommit *commitment, - commitSig lnwire.Sig, htlcSigs []lnwire.Sig) *channeldb.CommitDiff { + commitSig lnwire.Sig, htlcSigs []lnwire.Sig, + auxSigs []fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) { var ( logUpdates []channeldb.LogUpdate @@ -3341,21 +3377,71 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, // disk. diskCommit := newCommit.toDiskCommit(lntypes.Remote) - return &channeldb.CommitDiff{ - Commitment: *diskCommit, - CommitSig: &lnwire.CommitSig{ - ChanID: lnwire.NewChanIDFromOutPoint( - lc.channelState.FundingOutpoint, - ), - CommitSig: commitSig, - HtlcSigs: htlcSigs, + // We prepare the commit sig message to be sent to the remote party. + commitSigMsg := &lnwire.CommitSig{ + ChanID: lnwire.NewChanIDFromOutPoint( + lc.channelState.FundingOutpoint, + ), + CommitSig: commitSig, + HtlcSigs: htlcSigs, + } + + // Encode and check the size of the custom records now. + auxCustomRecords, err := fn.MapOptionZ( + lc.auxSigner, + func(s AuxSigner) fn.Result[lnwire.CustomRecords] { + blobOption, err := s.PackSigs(auxSigs).Unpack() + if err != nil { + return fn.Err[lnwire.CustomRecords](err) + } + + // We now serialize the commit sig message without the + // custom records to make sure we have space for them. + var buf bytes.Buffer + err = commitSigMsg.Encode(&buf, 0) + if err != nil { + return fn.Err[lnwire.CustomRecords](err) + } + + // The number of available bytes is the max message size + // minus the size of the message without the custom + // records. We also subtract 8 bytes for encoding + // overhead of the custom records (just some safety + // padding). + available := lnwire.MaxMsgBody - buf.Len() - 8 + + blob := blobOption.UnwrapOr(nil) + if len(blob) > available { + err = fmt.Errorf("aux sigs size %d exceeds "+ + "max allowed size of %d", len(blob), + available) + + return fn.Err[lnwire.CustomRecords](err) + } + + records, err := lnwire.ParseCustomRecords(blob) + if err != nil { + return fn.Err[lnwire.CustomRecords](err) + } + + return fn.Ok(records) }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + + commitSigMsg.CustomRecords = auxCustomRecords + + return &channeldb.CommitDiff{ + Commitment: *diskCommit, + CommitSig: commitSigMsg, LogUpdates: logUpdates, OpenedCircuitKeys: openCircuitKeys, ClosedCircuitKeys: closedCircuitKeys, AddAcks: ackAddRefs, SettleFailAcks: settleFailRefs, - } + }, nil } // getUnsignedAckedUpdates returns all remote log updates that we haven't @@ -3748,6 +3834,10 @@ type CommitSigs struct { // PartialSig is the musig2 partial signature for taproot commitment // transactions. PartialSig lnwire.OptPartialSigWithNonceTLV + + // AuxSigBlob is the blob containing all the auxiliary signatures for + // this new commitment state. + AuxSigBlob tlv.Blob } // NewCommitState wraps the various signatures needed to properly @@ -3772,6 +3862,8 @@ type NewCommitState struct { // any). The HTLC signatures are sorted according to the BIP 69 order of the // HTLC's on the commitment transaction. Finally, the new set of pending HTLCs // for the remote party's commitment are also returned. +// +//nolint:funlen func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { lc.Lock() defer lc.Unlock() @@ -3864,15 +3956,37 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - sigBatch, cancelChan, err := genRemoteHtlcSigJobs( + sigBatch, auxSigBatch, cancelChan, err := genRemoteHtlcSigJobs( keyRing, lc.channelState, leaseExpiry, newCommitView, lc.leafStore, ) if err != nil { return nil, err } + + // We'll need to send over the signatures to the remote party in the + // order as they appear on the commitment transaction after BIP 69 + // sorting. + slices.SortFunc(sigBatch, func(i, j SignJob) int { + return int(i.OutputIndex - j.OutputIndex) + }) + slices.SortFunc(auxSigBatch, func(i, j AuxSigJob) int { + return int(i.OutputIndex - j.OutputIndex) + }) + lc.sigPool.SubmitSignBatch(sigBatch) + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.SubmitSecondLevelSigBatch( + NewAuxChanState(lc.channelState), newCommitView.txn, + auxSigBatch, + ) + }) + if err != nil { + return nil, fmt.Errorf("error submitting second level sig "+ + "batch: %w", err) + } + // While the jobs are being carried out, we'll Sign their version of // the new commitment transaction while we're waiting for the rest of // the HTLC signatures to be processed. @@ -3910,17 +4024,12 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } } - // We'll need to send over the signatures to the remote party in the - // order as they appear on the commitment transaction after BIP 69 - // sorting. - sort.Slice(sigBatch, func(i, j int) bool { - return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex - }) - - // With the jobs sorted, we'll now iterate through all the responses to - // gather each of the signatures in order. + // Iterate through all the responses to gather each of the signatures + // in the order they were submitted. htlcSigs = make([]lnwire.Sig, 0, len(sigBatch)) - for _, htlcSigJob := range sigBatch { + auxSigs := make([]fn.Option[tlv.Blob], 0, len(auxSigBatch)) + for i := range sigBatch { + htlcSigJob := sigBatch[i] jobResp := <-htlcSigJob.Resp // If an error occurred, then we'll cancel any other active @@ -3931,12 +4040,34 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } htlcSigs = append(htlcSigs, jobResp.Sig) + + if lc.auxSigner.IsNone() { + continue + } + + auxHtlcSigJob := auxSigBatch[i] + auxJobResp := <-auxHtlcSigJob.Resp + + // If an error occurred, then we'll cancel any other active + // jobs. + if auxJobResp.Err != nil { + close(cancelChan) + return nil, auxJobResp.Err + } + + auxSigs = append(auxSigs, auxJobResp.SigBlob) } // As we're about to proposer a new commitment state for the remote // party, we'll write this pending state to disk before we exit, so we // can retransmit it if necessary. - commitDiff := lc.createCommitDiff(newCommitView, sig, htlcSigs) + commitDiff, err := lc.createCommitDiff( + newCommitView, sig, htlcSigs, auxSigs, + ) + if err != nil { + return nil, err + } + err = lc.channelState.AppendRemoteCommitChain(commitDiff) if err != nil { return nil, err @@ -3950,11 +4081,18 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { // latest commitment update. lc.commitChains.Remote.addCommitment(newCommitView) + auxSigBlob, err := commitDiff.CommitSig.CustomRecords.Serialize() + if err != nil { + return nil, fmt.Errorf("unable to serialize aux sig blob: %w", + err) + } + return &NewCommitState{ CommitSigs: &CommitSigs{ CommitSig: sig, HtlcSigs: htlcSigs, PartialSig: lnwire.MaybePartialSigWithNonce(partialSig), + AuxSigBlob: auxSigBlob, }, PendingHTLCs: commitDiff.Commitment.Htlcs, }, nil @@ -3966,8 +4104,8 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { // each time. After we receive the channel reestablish message, we learn the // nonce we need to use for the remote party. As a result, we need to generate // the partial signature again with the new nonce. -func (lc *LightningChannel) resignMusigCommit(commitTx *wire.MsgTx, -) (lnwire.OptPartialSigWithNonceTLV, error) { +func (lc *LightningChannel) resignMusigCommit( + commitTx *wire.MsgTx) (lnwire.OptPartialSigWithNonceTLV, error) { remoteSession := lc.musigSessions.RemoteSession musig, err := remoteSession.SignCommit(commitTx) @@ -4172,13 +4310,23 @@ func (lc *LightningChannel) ProcessChanSyncMsg( // If we signed this state, then we'll accumulate // another update to send over. case err == nil: + customRecords, err := lnwire.ParseCustomRecords( + newCommit.AuxSigBlob, + ) + if err != nil { + sErr := fmt.Errorf("error parsing aux "+ + "sigs: %w", err) + return nil, nil, nil, sErr + } + commitSig := &lnwire.CommitSig{ ChanID: lnwire.NewChanIDFromOutPoint( lc.channelState.FundingOutpoint, ), - CommitSig: newCommit.CommitSig, - HtlcSigs: newCommit.HtlcSigs, - PartialSig: newCommit.PartialSig, + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, + PartialSig: newCommit.PartialSig, + CustomRecords: customRecords, } updates = append(updates, commitSig) @@ -4396,6 +4544,7 @@ func (lc *LightningChannel) computeView(view *HtlcView, // need this to determine which HTLCs are dust, and also the final fee // rate. view.FeePerKw = commitChain.tip().feePerKw + view.NextHeight = nextHeight // We evaluate the view at this stage, meaning settled and failed HTLCs // will remove their corresponding added HTLCs. The resulting filtered @@ -4471,7 +4620,8 @@ func (lc *LightningChannel) computeView(view *HtlcView, func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, leaseExpiry uint32, - leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) { + leafStore fn.Option[AuxLeafStore], auxSigner fn.Option[AuxSigner], + sigBlob fn.Option[tlv.Blob]) ([]VerifyJob, []AuxVerifyJob, error) { var ( isLocalInitiator = chanState.IsInitiator @@ -4490,6 +4640,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, numHtlcs := len(localCommitmentView.incomingHTLCs) + len(localCommitmentView.outgoingHTLCs) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs) diskCommit := localCommitmentView.toDiskCommit(lntypes.Local) auxResult, err := fn.MapOptionZ( @@ -4501,7 +4652,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, }, ).Unpack() if err != nil { - return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w", + err) + } + + // If we have a sig blob, then we'll attempt to map that to individual + // blobs for each HTLC we might need a signature for. + auxHtlcSigs, err := fn.MapOptionZ( + auxSigner, func(a AuxSigner) fn.Result[[]fn.Option[tlv.Blob]] { + return a.UnpackSigs(sigBlob) + }, + ).Unpack() + if err != nil { + return nil, nil, fmt.Errorf("error unpacking aux sigs: %w", + err) } // We'll iterate through each output in the commitment transaction, @@ -4514,6 +4678,9 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcIndex uint64 sigHash func() ([]byte, error) sig input.Signature + htlc *paymentDescriptor + incoming bool + auxLeaf input.AuxTapLeaf err error ) @@ -4523,10 +4690,12 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // If this output index is found within the incoming HTLC // index, then this means that we need to generate an HTLC // success transaction in order to validate the signature. + //nolint:lll case localCommitmentView.incomingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.incomingHTLCIndex[outputIndex] + htlc = localCommitmentView.incomingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex + incoming = true sigHash = func() ([]byte, error) { op := wire.OutPoint{ @@ -4592,7 +4761,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -4608,15 +4777,16 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig // Otherwise, if this is an outgoing HTLC, then we'll need to // generate a timeout transaction so we can verify the // signature presented. + //nolint:lll case localCommitmentView.outgoingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.outgoingHTLCIndex[outputIndex] + htlc = localCommitmentView.outgoingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex @@ -4687,7 +4857,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -4703,7 +4873,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig @@ -4719,17 +4889,40 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, SigHash: sigHash, }) + if len(auxHtlcSigs) > i { + auxSig := auxHtlcSigs[i] + auxVerifyJob := NewAuxVerifyJob( + auxSig, *keyRing, incoming, + newAuxHtlcDescriptor(htlc), + localCommitmentView.customBlob, auxLeaf, + ) + + if htlc.CustomRecords == nil { + htlc.CustomRecords = make(lnwire.CustomRecords) + } + + // As this HTLC has a custom signature associated with + // it, store it in the custom records map so we can + // write to disk later. + sigType := htlcCustomSigType.TypeVal() + htlc.CustomRecords[uint64(sigType)] = auxSig.UnwrapOr( + nil, + ) + + auxVerifyJobs = append(auxVerifyJobs, auxVerifyJob) + } + i++ } // If we received a number of HTLC signatures that doesn't match our // commitment, we'll return an error now. if len(htlcSigs) != i { - return nil, fmt.Errorf("number of htlc sig mismatch. "+ + return nil, nil, fmt.Errorf("number of htlc sig mismatch. "+ "Expected %v sigs, got %v", i, len(htlcSigs)) } - return verifyJobs, nil + return verifyJobs, auxVerifyJobs, nil } // InvalidCommitSigError is a struct that implements the error interface to @@ -4888,6 +5081,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { localCommitmentView.ourBalance, localCommitmentView.theirBalance, lnutils.SpewLogClosure(localCommitmentView.txn)) + var auxSigBlob fn.Option[tlv.Blob] + if commitSigs.AuxSigBlob != nil { + auxSigBlob = fn.Some(commitSigs.AuxSigBlob) + } + // As an optimization, we'll generate a series of jobs for the worker // pool to verify each of the HTLC signatures presented. Once // generated, we'll submit these jobs to the worker pool. @@ -4895,9 +5093,10 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - verifyJobs, err := genHtlcSigValidationJobs( + verifyJobs, auxVerifyJobs, err := genHtlcSigValidationJobs( lc.channelState, localCommitmentView, keyRing, - commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, + commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, lc.auxSigner, + auxSigBlob, ) if err != nil { return err @@ -5050,6 +5249,18 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { } } + // Now that we know all the normal sigs are valid, we'll also verify + // the aux jobs, if any exist. + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.VerifySecondLevelSigs( + NewAuxChanState(lc.channelState), localCommitTx, + auxVerifyJobs, + ) + }) + if err != nil { + return fmt.Errorf("unable to validate aux sigs: %w", err) + } + // The signature checks out, so we can now add the new commitment to // our local commitment chain. For regular channels, we can just // serialize the ECDSA sig. For taproot channels, we'll serialize the @@ -5782,8 +5993,9 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, defer lc.Unlock() if htlc.ID != lc.updateLogs.Remote.htlcCounter { - return 0, fmt.Errorf("ID %d on HTLC add does not match expected next "+ - "ID %d", htlc.ID, lc.updateLogs.Remote.htlcCounter) + return 0, fmt.Errorf("ID %d on HTLC add does not match "+ + "expected next ID %d", htlc.ID, + lc.updateLogs.Remote.htlcCounter) } pd := &paymentDescriptor{ diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 744be3ae5..c47f87710 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -702,6 +703,68 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) { require.NoError(t, err, "unable to receive bob's commitment") } +// TestCommitHTLCSigCustomRecordSize asserts that custom records produced for +// a commitment_signed message are properly limited in size. +func TestCommitHTLCSigCustomRecordSize(t *testing.T) { + aliceChannel, bobChannel, err := CreateTestChannels( + t, channeldb.SimpleTaprootFeatureBit| + channeldb.TapscriptRootBit, + ) + require.NoError(t, err, "unable to create test channels") + + const ( + htlcAmt = lnwire.MilliSatoshi(20000000) + numHtlcs = 2 + ) + + largeRecords := lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType: bytes.Repeat([]byte{0}, 65_500), + } + largeBlob, err := largeRecords.Serialize() + require.NoError(t, err) + + aliceChannel.auxSigner.WhenSome(func(a AuxSigner) { + mockSigner, ok := a.(*MockAuxSigner) + require.True(t, ok, "expected MockAuxSigner") + + // Replace the default PackSigs implementation to return a + // large custom records blob. + mockSigner.ExpectedCalls = fn.Filter(func(c *mock.Call) bool { + return c.Method != "PackSigs" + }, mockSigner.ExpectedCalls) + mockSigner.On("PackSigs", mock.Anything). + Return(fn.Ok(fn.Some(largeBlob))) + }) + + // Add HTLCs with identical payment hashes and amounts, but descending + // CLTV values. We will expect the signatures to appear in the reverse + // order that the HTLCs are added due to the commitment sorting. + for i := 0; i < numHtlcs; i++ { + var ( + preimage lntypes.Preimage + hash = preimage.Hash() + ) + + htlc := &lnwire.UpdateAddHTLC{ + ID: uint64(i), + PaymentHash: hash, + Amount: htlcAmt, + Expiry: uint32(numHtlcs - i), + } + + if _, err := aliceChannel.AddHTLC(htlc, nil); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + t.Fatalf("bob unable to receive htlc: %v", err) + } + } + + // We expect an error because of the large custom records blob. + _, err = aliceChannel.SignNextCommitment() + require.ErrorContains(t, err, "exceeds max allowed size") +} + // TestCooperativeChannelClosure checks that the coop close process finishes // with an agreement from both parties, and that the final balances of the // close tx check out. @@ -3046,6 +3109,10 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) { return channelNew, nil } +// testChanSyncOweCommitment tests that if Bob restarts (and then Alice) before +// he receives Alice's CommitSig message, then Alice concludes that she needs +// to re-send the CommitDiff. After the diff has been sent, both nodes should +// resynchronize and be able to complete the dangling commit. func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) { // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, @@ -3210,8 +3277,10 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) { len(commitSigMsg.HtlcSigs)) } for i, htlcSig := range commitSigMsg.HtlcSigs { - if !bytes.Equal(htlcSig.RawBytes(), - aliceNewCommit.HtlcSigs[i].RawBytes()) { + if !bytes.Equal( + htlcSig.RawBytes(), + aliceNewCommit.HtlcSigs[i].RawBytes(), + ) { t.Fatalf("htlc sig msgs don't match: "+ "expected %v got %v", @@ -3389,6 +3458,100 @@ func TestChanSyncOweCommitment(t *testing.T) { } } +type testSigBlob struct { + BlobInt tlv.RecordT[tlv.TlvType65634, uint16] +} + +// TestChanSyncOweCommitmentAuxSigner tests that when one party owes a +// signature after a channel reest, if an aux signer is present, then the +// signature message sent includes the additional aux sigs as extra data. +func TestChanSyncOweCommitmentAuxSigner(t *testing.T) { + t.Parallel() + + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + chanType := channeldb.SingleFunderTweaklessBit | + channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit | + channeldb.TapscriptRootBit + + aliceChannel, bobChannel, err := CreateTestChannels(t, chanType) + require.NoError(t, err, "unable to create test channels") + + // We'll now manually attach an aux signer to Alice's channel. + auxSigner := &MockAuxSigner{} + aliceChannel.auxSigner = fn.Some[AuxSigner](auxSigner) + + var fakeOnionBlob [lnwire.OnionPacketSize]byte + copy( + fakeOnionBlob[:], + bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize), + ) + + // To kick things off, we'll have Alice send a single HTLC to Bob. + htlcAmt := lnwire.NewMSatFromSatoshis(20000) + var bobPreimage [32]byte + copy(bobPreimage[:], bytes.Repeat([]byte{0}, 32)) + rHash := sha256.Sum256(bobPreimage[:]) + h := &lnwire.UpdateAddHTLC{ + PaymentHash: rHash, + Amount: htlcAmt, + Expiry: uint32(10), + OnionBlob: fakeOnionBlob, + } + + _, err = aliceChannel.AddHTLC(h, nil) + require.NoError(t, err, "unable to recv bob's htlc: %v", err) + + // We'll set up the mock to expect calls to PackSigs and also + // SubmitSubmitSecondLevelSigBatch. + var sigBlobBuf bytes.Buffer + sigBlob := testSigBlob{ + BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5), + } + tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record()) + require.NoError(t, err, "unable to create tlv stream") + require.NoError(t, tlvStream.Encode(&sigBlobBuf)) + + auxSigner.On( + "SubmitSecondLevelSigBatch", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil).Twice() + auxSigner.On( + "PackSigs", mock.Anything, + ).Return( + fn.Ok(fn.Some(sigBlobBuf.Bytes())), nil, + ) + + _, err = aliceChannel.SignNextCommitment() + require.NoError(t, err, "unable to sign commitment") + + _, err = aliceChannel.GenMusigNonces() + require.NoError(t, err, "unable to generate musig nonces") + + // Next we'll simulate a restart, by having Bob send over a chan sync + // message to Alice. + bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg() + require.NoError(t, err, "unable to produce chan sync msg") + + aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg( + bobSyncMsg, + ) + require.NoError(t, err) + require.Len(t, aliceMsgsToSend, 2) + + // The first message should be an update add HTLC. + require.IsType(t, &lnwire.UpdateAddHTLC{}, aliceMsgsToSend[0]) + + // The second should be a commit sig message. + sigMsg, ok := aliceMsgsToSend[1].(*lnwire.CommitSig) + require.True(t, ok) + require.True(t, sigMsg.PartialSig.IsSome()) + + // The signature should have the CustomRecords field set. + require.NotEmpty(t, sigMsg.CustomRecords) +} + func testChanSyncOweCommitmentPendingRemote(t *testing.T, chanType channeldb.ChannelType) { @@ -3398,7 +3561,10 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T, require.NoError(t, err, "unable to create test channels") var fakeOnionBlob [lnwire.OnionPacketSize]byte - copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize)) + copy( + fakeOnionBlob[:], + bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize), + ) // We'll start off the scenario where Bob send two htlcs to Alice in a // single state update. @@ -3437,7 +3603,9 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T, // Next, Alice settles the HTLCs from Bob in distinct state updates. for i := 0; i < numHtlcs; i++ { - err = aliceChannel.SettleHTLC(preimages[i], uint64(i), nil, nil, nil) + err = aliceChannel.SettleHTLC( + preimages[i], uint64(i), nil, nil, nil, + ) if err != nil { t.Fatalf("unable to settle htlc: %v", err) } @@ -3727,7 +3895,7 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) { } // TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before -// he receiver's Alice's RevokeAndAck message, then Alice concludes that she +// he received Alice's RevokeAndAck message, then Alice concludes that she // needs to re-send the RevokeAndAck. After the revocation has been sent, both // nodes should be able to successfully complete another state transition. func TestChanSyncOweRevocation(t *testing.T) { diff --git a/lnwallet/config.go b/lnwallet/config.go index 24961f38e..425fe15da 100644 --- a/lnwallet/config.go +++ b/lnwallet/config.go @@ -67,4 +67,8 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[AuxSigner] } diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 2afff4f21..82b9e19c2 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -21,6 +21,7 @@ import ( "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" ) var ( @@ -441,3 +442,54 @@ func (*MockAuxLeafStore) ApplyHtlcView( return fn.Ok(fn.None[tlv.Blob]()) } + +// MockAuxSigner is a mock implementation of the AuxSigner interface. +type MockAuxSigner struct { + mock.Mock +} + +// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and +// processes them asynchronously. +func (a *MockAuxSigner) SubmitSecondLevelSigBatch(chanState AuxChanState, + tx *wire.MsgTx, jobs []AuxSigJob) error { + + args := a.Called(chanState, tx, jobs) + + // While we return, we'll also send back an instant response for the + // set of jobs. + for _, sigJob := range jobs { + sigJob.Resp <- AuxSigJobResp{} + } + + return args.Error(0) +} + +// PackSigs takes a series of aux signatures and packs them into a +// single blob that can be sent alongside the CommitSig messages. +func (a *MockAuxSigner) PackSigs( + sigs []fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]] { + + args := a.Called(sigs) + + return args.Get(0).(fn.Result[fn.Option[tlv.Blob]]) +} + +// UnpackSigs takes a packed blob of signatures and returns the +// original signatures for each HTLC, keyed by HTLC index. +func (a *MockAuxSigner) UnpackSigs( + sigs fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]] { + + args := a.Called(sigs) + + return args.Get(0).(fn.Result[[]fn.Option[tlv.Blob]]) +} + +// VerifySecondLevelSigs attempts to synchronously verify a batch of aux +// sig jobs. +func (a *MockAuxSigner) VerifySecondLevelSigs(chanState AuxChanState, + tx *wire.MsgTx, jobs []AuxVerifyJob) error { + + args := a.Called(chanState, tx, jobs) + + return args.Error(0) +} diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 1f0000e8e..7f1a89c22 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" @@ -25,7 +26,7 @@ type CommitmentType int const ( // CommitmentTypeLegacy is the legacy commitment format with a tweaked // to_remote key. - CommitmentTypeLegacy = iota + CommitmentTypeLegacy CommitmentType = iota // CommitmentTypeTweakless is a newer commitment format where the // to_remote key is static. @@ -100,6 +101,28 @@ func (c CommitmentType) String() string { } } +// ReservationState is a type that represents the current state of a channel +// reservation within the funding workflow. +type ReservationState int + +const ( + // WaitingToSend is the state either the funder/fundee is in after + // creating a reservation, but hasn't sent a message yet. + WaitingToSend ReservationState = iota + + // SentOpenChannel is the state the funder is in after sending the + // OpenChannel message. + SentOpenChannel + + // SentAcceptChannel is the state the fundee is in after sending the + // AcceptChannel message. + SentAcceptChannel + + // SentFundingCreated is the state the funder is in after sending the + // FundingCreated message. + SentFundingCreated +) + // ChannelContribution is the primary constituent of the funding workflow // within lnwallet. Each side first exchanges their respective contributions // along with channel specific parameters like the min fee/KB. Once @@ -223,6 +246,8 @@ type ChannelReservation struct { nextRevocationKeyLoc keychain.KeyLocator musigSessions *MusigPairSession + + state ReservationState } // NewChannelReservation creates a new channel reservation. This function is @@ -459,6 +484,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, reservationID: id, wallet: wallet, chanFunder: req.ChanFunder, + state: WaitingToSend, }, nil } @@ -470,6 +496,22 @@ func (r *ChannelReservation) AddAlias(scid lnwire.ShortChannelID) { r.partialState.ShortChannelID = scid } +// SetState sets the ReservationState. +func (r *ChannelReservation) SetState(state ReservationState) { + r.Lock() + defer r.Unlock() + + r.state = state +} + +// State returns the current ReservationState. +func (r *ChannelReservation) State() ReservationState { + r.RLock() + defer r.RUnlock() + + return r.state +} + // SetNumConfsRequired sets the number of confirmations that are required for // the ultimate funding transaction before the channel can be considered open. // This is distinct from the main reservation workflow as it allows @@ -610,12 +652,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, } @@ -717,8 +762,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) @@ -728,6 +775,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi fundingOutpoint: fundingPoint, theirCommitmentSig: commitSig, completeChan: completeChan, + auxFundingDesc: auxFundingDesc, err: errChan, } @@ -813,6 +861,42 @@ 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. +// +//nolint:lll +func (r *ChannelReservation) CommitmentKeyRings() lntypes.Dual[CommitmentKeyRing] { + r.RLock() + defer r.RUnlock() + + chanType := r.partialState.ChanType + ourChanCfg := r.ourContribution.ChannelConfig + theirChanCfg := r.theirContribution.ChannelConfig + + localKeys := DeriveCommitmentKeys( + r.ourContribution.FirstCommitmentPoint, lntypes.Local, chanType, + ourChanCfg, theirChanCfg, + ) + + remoteKeys := DeriveCommitmentKeys( + r.theirContribution.FirstCommitmentPoint, lntypes.Remote, + chanType, ourChanCfg, theirChanCfg, + ) + + return lntypes.Dual[CommitmentKeyRing]{ + Local: *localKeys, + Remote: *remoteKeys, + } +} + // VerifyConstraints is a helper function that can be used to check the sanity // of various channel constraints. func VerifyConstraints(bounds *channeldb.ChannelStateBounds, diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go index 2424757f9..2296e1703 100644 --- a/lnwallet/sigpool.go +++ b/lnwallet/sigpool.go @@ -45,17 +45,17 @@ type VerifyJob struct { // party's update log. HtlcIndex uint64 - // Cancel is a channel that should be closed if the caller wishes to + // Cancel is a channel that is closed by the caller if they wish to // cancel all pending verification jobs part of a single batch. This - // channel is to be closed in the case that a single signature in a - // batch has been returned as invalid, as there is no need to verify - // the remainder of the signatures. - Cancel chan struct{} + // channel is closed in the case that a single signature in a batch has + // been returned as invalid, as there is no need to verify the remainder + // of the signatures. + Cancel <-chan struct{} // ErrResp is the channel that the result of the signature verification // is to be sent over. In the see that the signature is valid, a nil // error will be passed. Otherwise, a concrete error detailing the - // issue will be passed. + // issue will be passed. This channel MUST be buffered. ErrResp chan *HtlcIndexErr } @@ -86,12 +86,13 @@ type SignJob struct { // transaction being signed. OutputIndex int32 - // Cancel is a channel that should be closed if the caller wishes to - // abandon all pending sign jobs part of a single batch. - Cancel chan struct{} + // Cancel is a channel that is closed by the caller if they wish to + // abandon all pending sign jobs part of a single batch. This should + // never be closed by the validator. + Cancel <-chan struct{} // Resp is the channel that the response to this particular SignJob - // will be sent over. + // will be sent over. This channel MUST be buffered. // // TODO(roasbeef): actually need to allow caller to set, need to retain // order mark commit sig as special diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index bcb396caf..4a02f0324 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" @@ -940,6 +941,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/test_utils.go b/lnwallet/test_utils.go index a9f71f24c..d4f0d05ae 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -1,6 +1,7 @@ package lnwallet import ( + "bytes" "crypto/rand" "encoding/binary" "encoding/hex" @@ -21,6 +22,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -369,9 +372,13 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, // TODO(roasbeef): make mock version of pre-image store + auxSigner := NewDefaultAuxSignerMock(t) + alicePool := NewSigPool(1, aliceSigner) channelAlice, err := NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) if err != nil { return nil, nil, err @@ -386,6 +393,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, bobPool := NewSigPool(1, bobSigner) channelBob, err := NewLightningChannel( bobSigner, bobChannelState, bobPool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) if err != nil { return nil, nil, err @@ -586,3 +595,38 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { return nil } + +func NewDefaultAuxSignerMock(t *testing.T) *MockAuxSigner { + auxSigner := &MockAuxSigner{} + + type testSigBlob struct { + BlobInt tlv.RecordT[tlv.TlvType65634, uint16] + } + + var sigBlobBuf bytes.Buffer + sigBlob := testSigBlob{ + BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5), + } + tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record()) + require.NoError(t, err, "unable to create tlv stream") + require.NoError(t, tlvStream.Encode(&sigBlobBuf)) + + auxSigner.On( + "SubmitSecondLevelSigBatch", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil) + auxSigner.On( + "PackSigs", mock.Anything, + ).Return(fn.Ok(fn.Some(sigBlobBuf.Bytes()))) + auxSigner.On( + "UnpackSigs", mock.Anything, + ).Return(fn.Ok([]fn.Option[tlv.Blob]{ + fn.Some(sigBlobBuf.Bytes()), + })) + auxSigner.On( + "VerifySecondLevelSigs", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil) + + return auxSigner +} diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 3588acfeb..8786c2d5d 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -1018,9 +1018,12 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2, }, nil) + auxSigner := NewDefaultAuxSignerMock(t) remotePool := NewSigPool(1, remoteSigner) channelRemote, err := NewLightningChannel( remoteSigner, remoteChannelState, remotePool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) require.NoError(t, err) require.NoError(t, remotePool.Start()) @@ -1028,6 +1031,8 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp localPool := NewSigPool(1, localSigner) channelLocal, err := NewLightningChannel( localSigner, localChannelState, localPool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) require.NoError(t, err) require.NoError(t, localPool.Start()) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 7e455ab48..f60856113 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -32,6 +32,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chanvalidate" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" ) const ( @@ -90,6 +91,33 @@ func (p *PsbtFundingRequired) Error() string { return ErrPsbtFundingRequired.Error() } +// AuxFundingDesc stores a series of attributes that may be used to modify the +// way the channel funding occurs. This struct contains information that can +// only be derived once both sides have received and sent their contributions +// to the channel (keys, etc.). +type AuxFundingDesc struct { + // CustomFundingBlob is a custom blob that'll be stored in the database + // within the OpenChannel struct. This should represent information + // static to the channel lifetime. + CustomFundingBlob tlv.Blob + + // CustomLocalCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the local party. + CustomLocalCommitBlob tlv.Blob + + // CustomRemoteCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the remote party. + CustomRemoteCommitBlob tlv.Blob + + // 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 // required to open a payment channel with a remote peer. The initial required // parameters are configurable across channels. These parameters are to be @@ -211,9 +239,8 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte - // TapscriptRoot is the root of the tapscript tree that will be used to - // create the funding output. This is an optional field that should - // only be set for taproot channels. + // TapscriptRoot is an optional tapscript root that if provided, will + // be used to create the combined key for musig2 based channels. TapscriptRoot fn.Option[chainhash.Hash] // err is a channel in which all errors will be sent across. Will be @@ -251,7 +278,6 @@ type fundingReserveCancelMsg struct { type addContributionMsg struct { pendingFundingID uint64 - // TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode contribution *ChannelContribution // NOTE: In order to avoid deadlocks, this channel MUST be buffered. @@ -264,6 +290,10 @@ type addContributionMsg struct { type continueContributionMsg struct { pendingFundingID uint64 + // auxFundingDesc is an optional descriptor that contains information + // about the custom channel funding flow. + auxFundingDesc fn.Option[AuxFundingDesc] + // NOTE: In order to avoid deadlocks, this channel MUST be buffered. err chan error } @@ -319,6 +349,10 @@ type addCounterPartySigsMsg struct { type addSingleFunderSigsMsg struct { pendingFundingID uint64 + // auxFundingDesc is an optional descriptor that contains information + // about the custom channel funding flow. + auxFundingDesc fn.Option[AuxFundingDesc] + // fundingOutpoint is the outpoint of the completed funding // transaction as assembled by the workflow initiator. fundingOutpoint *wire.OutPoint @@ -422,8 +456,6 @@ type LightningWallet struct { quit chan struct{} wg sync.WaitGroup - - // TODO(roasbeef): handle wallet lock/unlock } // NewLightningWallet creates/opens and initializes a LightningWallet instance. @@ -468,7 +500,6 @@ func (l *LightningWallet) Startup() error { } l.wg.Add(1) - // TODO(roasbeef): multiple request handlers? go l.requestHandler() return nil @@ -1424,7 +1455,6 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, // transaction via coin selection are freed allowing future reservations to // include them. func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) { - // TODO(roasbeef): holding lock too long l.limboMtx.Lock() defer l.limboMtx.Unlock() @@ -1449,11 +1479,6 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs ) } - // TODO(roasbeef): is it even worth it to keep track of unused keys? - - // TODO(roasbeef): Is it possible to mark the unused change also as - // available? - delete(l.fundingLimbo, req.pendingFundingID) pid := pendingReservation.pendingChanID @@ -1473,7 +1498,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // createCommitOpts is a struct that holds the options for creating 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. @@ -1481,6 +1507,17 @@ func defaultCommitOpts() createCommitOpts { return createCommitOpts{} } +// WithAuxLeaves is a functional option that can be used to set the aux leaves +// for a new commitment transaction. +func WithAuxLeaves(localLeaves, + remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { + + return func(o *createCommitOpts) { + o.localAuxLeaves = localLeaves + o.remoteAuxLeaves = remoteLeaves + } +} + // CreateCommitOpt is a functional option that can be used to modify the way a // new commitment transaction is created. type CreateCommitOpt func(*createCommitOpts) @@ -1514,7 +1551,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 @@ -1528,7 +1565,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 @@ -1621,16 +1658,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // and remote key which will be needed to calculate the multisig // funding output in a next step. pendingChanID := pendingReservation.pendingChanID + walletLog.Debugf("Advancing PSBT funding flow for "+ "pending_id(%x), binding keys local_key=%v, "+ "remote_key=%x", pendingChanID, &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey.SerializeCompressed()) + fundingIntent.BindKeys( &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey, ) + // We might have a tapscript root, so we'll bind that now to + // ensure we make the proper funding output. + fundingIntent.BindTapscriptRoot( + pendingReservation.partialState.TapscriptRoot, + ) + // Exit early because we can't continue the funding flow yet. req.err <- &PsbtFundingRequired{ Intent: fundingIntent, @@ -1703,16 +1748,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // the commitment transaction for the remote party, and verify their incoming // partial signature. func genMusigSession(ourContribution, theirContribution *ChannelContribution, - signer input.MuSig2Signer, - fundingOutput *wire.TxOut) *MusigPairSession { + signer input.MuSig2Signer, fundingOutput *wire.TxOut, + tapscriptRoot fn.Option[chainhash.Hash]) *MusigPairSession { return NewMusigPairSession(&MusigSessionCfg{ - LocalKey: ourContribution.MultiSigKey, - RemoteKey: theirContribution.MultiSigKey, - LocalNonce: *ourContribution.LocalNonce, - RemoteNonce: *theirContribution.LocalNonce, - Signer: signer, - InputTxOut: fundingOutput, + LocalKey: ourContribution.MultiSigKey, + RemoteKey: theirContribution.MultiSigKey, + LocalNonce: *ourContribution.LocalNonce, + RemoteNonce: *theirContribution.LocalNonce, + Signer: signer, + InputTxOut: fundingOutput, + TapscriptTweak: tapscriptRoot, }) } @@ -1762,6 +1808,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, musigSessions := genMusigSession( ourContribution, theirContribution, l.Cfg.Signer, fundingOutput, + pendingReservation.partialState.TapscriptRoot, ) pendingReservation.musigSessions = musigSessions } @@ -1797,6 +1844,26 @@ 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 @@ -1855,7 +1922,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 @@ -1871,6 +1937,18 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { 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, ourContribution.ChannelConfig, theirContribution.ChannelConfig, @@ -1878,6 +1956,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2138,6 +2217,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, res.musigSessions = genMusigSession( res.ourContribution, res.theirContribution, l.Cfg.Signer, fundingOutput, + res.partialState.TapscriptRoot, ) } @@ -2228,9 +2308,6 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // As we're about to broadcast the funding transaction, we'll take note // of the current height for record keeping purposes. - // - // TODO(roasbeef): this info can also be piped into light client's - // basic fee estimation? _, bestHeight, err := l.Cfg.ChainIO.GetBestBlock() if err != nil { msg.err <- err @@ -2291,6 +2368,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) @@ -2304,6 +2398,18 @@ 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, @@ -2312,6 +2418,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2500,6 +2607,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) { chanOpts = append(chanOpts, WithLeafStore(s)) }) + l.Cfg.AuxSigner.WhenSome(func(s AuxSigner) { + chanOpts = append(chanOpts, WithAuxSigner(s)) + }) // First, we'll obtain a fully signed commitment transaction so we can // pass into it on the chanvalidate package for verification. diff --git a/lnwire/commit_sig.go b/lnwire/commit_sig.go index 7deb64ae1..3a475e71f 100644 --- a/lnwire/commit_sig.go +++ b/lnwire/commit_sig.go @@ -45,6 +45,10 @@ type CommitSig struct { // being signed for. In this case, the above Sig type MUST be blank. PartialSig OptPartialSigWithNonceTLV + // CustomRecords maps TLV types to byte slices, storing arbitrary data + // intended for inclusion in the ExtraData field. + CustomRecords CustomRecords + // ExtraData is the set of data that was appended to this message to // fill out the full maximum transport message size. These fields can // be used to specify optional data such as custom TLV fields. @@ -53,9 +57,7 @@ type CommitSig struct { // NewCommitSig creates a new empty CommitSig message. func NewCommitSig() *CommitSig { - return &CommitSig{ - ExtraData: make([]byte, 0), - } + return &CommitSig{} } // A compile time check to ensure CommitSig implements the lnwire.Message @@ -67,34 +69,37 @@ var _ Message = (*CommitSig)(nil) // // This is part of the lnwire.Message interface. func (c *CommitSig) Decode(r io.Reader, pver uint32) error { + // msgExtraData is a temporary variable used to read the message extra + // data field from the reader. + var msgExtraData ExtraOpaqueData + err := ReadElements(r, &c.ChanID, &c.CommitSig, &c.HtlcSigs, + &msgExtraData, ) if err != nil { return err } - var tlvRecords ExtraOpaqueData - if err := ReadElements(r, &tlvRecords); err != nil { - return err - } - + // Extract TLV records from the extra data field. partialSig := c.PartialSig.Zero() - typeMap, err := tlvRecords.ExtractRecords(&partialSig) + + customRecords, parsed, extraData, err := ParseAndExtractCustomRecords( + msgExtraData, &partialSig, + ) if err != nil { return err } // Set the corresponding TLV types if they were included in the stream. - if val, ok := typeMap[c.PartialSig.TlvType()]; ok && val == nil { + if _, ok := parsed[partialSig.TlvType()]; ok { c.PartialSig = tlv.SomeRecordT(partialSig) } - if len(tlvRecords) != 0 { - c.ExtraData = tlvRecords - } + c.CustomRecords = customRecords + c.ExtraData = extraData return nil } @@ -108,7 +113,10 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error { c.PartialSig.WhenSome(func(sig PartialSigWithNonceTLV) { recordProducers = append(recordProducers, &sig) }) - err := EncodeMessageExtraData(&c.ExtraData, recordProducers...) + + extraData, err := MergeAndEncode( + recordProducers, c.ExtraData, c.CustomRecords, + ) if err != nil { return err } @@ -125,7 +133,7 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error { return err } - return WriteBytes(w, c.ExtraData) + return WriteBytes(w, extraData) } // MsgType returns the integer uniquely identifying this message type on the diff --git a/lnwire/commit_sig_test.go b/lnwire/commit_sig_test.go new file mode 100644 index 000000000..0772a2fb8 --- /dev/null +++ b/lnwire/commit_sig_test.go @@ -0,0 +1,168 @@ +package lnwire + +import ( + "bytes" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/require" +) + +// testCase is a test case for the CommitSig message. +type commitSigTestCase struct { + // Msg is the message to be encoded and decoded. + Msg CommitSig + + // ExpectEncodeError is a flag that indicates whether we expect the + // encoding of the message to fail. + ExpectEncodeError bool +} + +// generateCommitSigTestCases generates a set of CommitSig message test cases. +func generateCommitSigTestCases(t *testing.T) []commitSigTestCase { + // Firstly, we'll set basic values for the message fields. + // + // Generate random channel ID. + chanIDBytes, err := generateRandomBytes(32) + require.NoError(t, err) + + var chanID ChannelID + copy(chanID[:], chanIDBytes) + + // Generate random commit sig. + commitSigBytes, err := generateRandomBytes(64) + require.NoError(t, err) + + sig, err := NewSigFromSchnorrRawSignature(commitSigBytes) + require.NoError(t, err) + + sigScalar := new(btcec.ModNScalar) + sigScalar.SetByteSlice(sig.RawBytes()) + + var nonce [musig2.PubNonceSize]byte + copy(nonce[:], commitSigBytes) + + sigWithNonce := NewPartialSigWithNonce(nonce, *sigScalar) + partialSig := MaybePartialSigWithNonce(sigWithNonce) + + // Define custom records. + recordKey1 := uint64(MinCustomRecordsTlvType + 1) + recordValue1, err := generateRandomBytes(10) + require.NoError(t, err) + + recordKey2 := uint64(MinCustomRecordsTlvType + 2) + recordValue2, err := generateRandomBytes(10) + require.NoError(t, err) + + customRecords := CustomRecords{ + recordKey1: recordValue1, + recordKey2: recordValue2, + } + + // Construct an instance of extra data that contains records with TLV + // types below the minimum custom records threshold and that lack + // corresponding fields in the message struct. Content should persist in + // the extra data field after encoding and decoding. + var ( + recordBytes45 = []byte("recordBytes45") + tlvRecord45 = tlv.NewPrimitiveRecord[tlv.TlvType45]( + recordBytes45, + ) + + recordBytes55 = []byte("recordBytes55") + tlvRecord55 = tlv.NewPrimitiveRecord[tlv.TlvType55]( + recordBytes55, + ) + ) + + var extraData ExtraOpaqueData + err = extraData.PackRecords( + []tlv.RecordProducer{&tlvRecord45, &tlvRecord55}..., + ) + require.NoError(t, err) + + invalidCustomRecords := CustomRecords{ + MinCustomRecordsTlvType - 1: recordValue1, + } + + return []commitSigTestCase{ + { + Msg: CommitSig{ + ChanID: chanID, + CommitSig: sig, + PartialSig: partialSig, + CustomRecords: customRecords, + ExtraData: extraData, + }, + }, + // Add a test case where the blinding point field is not + // populated. + { + Msg: CommitSig{ + ChanID: chanID, + CommitSig: sig, + CustomRecords: customRecords, + }, + }, + // Add a test case where the custom records field is not + // populated. + { + Msg: CommitSig{ + ChanID: chanID, + CommitSig: sig, + PartialSig: partialSig, + }, + }, + // Add a case where the custom records are invalid. + { + Msg: CommitSig{ + ChanID: chanID, + CommitSig: sig, + PartialSig: partialSig, + CustomRecords: invalidCustomRecords, + }, + ExpectEncodeError: true, + }, + } +} + +// TestCommitSigEncodeDecode tests CommitSig message encoding and decoding for +// all supported field values. +func TestCommitSigEncodeDecode(t *testing.T) { + t.Parallel() + + // Generate test cases. + testCases := generateCommitSigTestCases(t) + + // Execute test cases. + for tcIdx, tc := range testCases { + t.Run(fmt.Sprintf("testcase-%d", tcIdx), func(t *testing.T) { + // Encode test case message. + var buf bytes.Buffer + err := tc.Msg.Encode(&buf, 0) + + // Check if we expect an encoding error. + if tc.ExpectEncodeError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // Decode the encoded message bytes message. + var actualMsg CommitSig + decodeReader := bytes.NewReader(buf.Bytes()) + err = actualMsg.Decode(decodeReader, 0) + require.NoError(t, err) + + // The signature type isn't serialized. + actualMsg.CommitSig.ForceSchnorr() + + // Compare the two messages to ensure equality. + require.Equal(t, tc.Msg, actualMsg) + }) + } +} diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index a5a754806..63742b4a5 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -965,6 +965,8 @@ func TestLightningWireProtocol(t *testing.T) { } } + req.CustomRecords = randCustomRecords(t, r) + // 50/50 chance to attach a partial sig. if r.Int31()%2 == 0 { req.PartialSig = somePartialSigWithNonce(t, r) diff --git a/peer/brontide.go b/peer/brontide.go index 5669193a2..99d38a855 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -376,6 +376,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // PongBuf is a slice we'll reuse instead of allocating memory on the // heap. Since only reads will occur and no writes, there is no need // for any synchronization primitives. As a result, it's safe to share @@ -952,6 +956,9 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) ( p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) lnChan, err := lnwallet.NewLightningChannel( p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts..., ) @@ -4164,6 +4171,9 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error { p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // If not already active, we'll add this channel to the set of active // channels, so we can look it up later easily according to its channel diff --git a/peer/test_utils.go b/peer/test_utils.go index 0575acca5..231a476ce 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -304,6 +304,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a, alicePool := lnwallet.NewSigPool(1, aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}), ) if err != nil { return nil, err @@ -316,6 +318,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a, bobPool := lnwallet.NewSigPool(1, bobSigner) channelBob, err := lnwallet.NewLightningChannel( bobSigner, bobChannelState, bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}), ) if err != nil { return nil, err diff --git a/rpcserver.go b/rpcserver.go index d46e04741..3b8a9ea00 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -87,6 +87,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "gopkg.in/macaroon-bakery.v2/bakery" ) @@ -579,6 +580,17 @@ func MainRPCServerPermissions() map[string][]bakery.Op { } } +// AuxDataParser is an interface that is used to parse auxiliary custom data +// within RPC messages. This is used to transform binary blobs to human-readable +// JSON representations. +type AuxDataParser interface { + // InlineParseCustomData replaces any custom data binary blob in the + // given RPC message with its corresponding JSON formatted data. This + // transforms the binary (likely TLV encoded) data to a human-readable + // JSON representation (still as byte slice). + InlineParseCustomData(msg proto.Message) error +} + // rpcServer is a gRPC, RPC front end to the lnd daemon. // TODO(roasbeef): pagination support for the list-style calls type rpcServer struct { @@ -731,6 +743,20 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, }, SetChannelAuto: s.chanStatusMgr.RequestAuto, UseStatusInitiated: subServerCgs.RouterRPC.UseStatusInitiated, + ParseCustomChannelData: func(msg proto.Message) error { + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(msg) + }, + ) + if err != nil { + return fmt.Errorf("error parsing custom data: "+ + "%w", err) + } + + return nil + }, } genInvoiceFeatures := func() *lnwire.FeatureVector { @@ -3529,6 +3555,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, unsettledRemoteBalance lnwire.MilliSatoshi pendingOpenLocalBalance lnwire.MilliSatoshi pendingOpenRemoteBalance lnwire.MilliSatoshi + customDataBuf bytes.Buffer ) openChannels, err := r.server.chanStateDB.FetchAllOpenChannels() @@ -3536,6 +3563,12 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, return nil, err } + // Encode the number of open channels to the custom data buffer. + err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(openChannels))) + if err != nil { + return nil, err + } + for _, channel := range openChannels { c := channel.LocalCommitment localBalance += c.LocalBalance @@ -3549,6 +3582,13 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, unsettledRemoteBalance += htlc.Amt } } + + // Encode the custom data for this open channel. + openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil) + err = wire.WriteVarBytes(&customDataBuf, 0, openChanData) + if err != nil { + return nil, err + } } pendingChannels, err := r.server.chanStateDB.FetchPendingChannels() @@ -3556,10 +3596,23 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, return nil, err } + // Encode the number of pending channels to the custom data buffer. + err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(pendingChannels))) + if err != nil { + return nil, err + } + for _, channel := range pendingChannels { c := channel.LocalCommitment pendingOpenLocalBalance += c.LocalBalance pendingOpenRemoteBalance += c.RemoteBalance + + // Encode the custom data for this pending channel. + openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil) + err = wire.WriteVarBytes(&customDataBuf, 0, openChanData) + if err != nil { + return nil, err + } } rpcsLog.Debugf("[channelbalance] local_balance=%v remote_balance=%v "+ @@ -3569,7 +3622,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, unsettledRemoteBalance, pendingOpenLocalBalance, pendingOpenRemoteBalance) - return &lnrpc.ChannelBalanceResponse{ + resp := &lnrpc.ChannelBalanceResponse{ LocalBalance: &lnrpc.Amount{ Sat: uint64(localBalance.ToSatoshis()), Msat: uint64(localBalance), @@ -3594,11 +3647,24 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, Sat: uint64(pendingOpenRemoteBalance.ToSatoshis()), Msat: uint64(pendingOpenRemoteBalance), }, + CustomChannelData: customDataBuf.Bytes(), // Deprecated fields. Balance: int64(localBalance.ToSatoshis()), PendingOpenBalance: int64(pendingOpenLocalBalance.ToSatoshis()), - }, nil + } + + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + + return resp, nil } type ( @@ -3654,6 +3720,12 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) { pendingChan.BroadcastHeight() fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight + customChanBytes, err := encodeCustomChanData(pendingChan) + if err != nil { + return nil, fmt.Errorf("unable to encode open chan "+ + "data: %w", err) + } + result[i] = &lnrpc.PendingChannelsResponse_PendingOpenChannel{ Channel: &lnrpc.PendingChannelsResponse_PendingChannel{ RemoteNodePub: hex.EncodeToString(pub), @@ -3667,6 +3739,7 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) { CommitmentType: rpcCommitmentType(pendingChan.ChanType), Private: isPrivate(pendingChan), Memo: string(pendingChan.Memo), + CustomChannelData: customChanBytes, }, CommitWeight: commitWeight, CommitFee: int64(localCommitment.CommitFee), @@ -4033,6 +4106,16 @@ func (r *rpcServer) PendingChannels(ctx context.Context, resp.WaitingCloseChannels = waitingCloseChannels resp.TotalLimboBalance += limbo + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + return resp, nil } @@ -4347,6 +4430,16 @@ func (r *rpcServer) ListChannels(ctx context.Context, resp.Channels = append(resp.Channels, channel) } + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + return resp, nil } @@ -4397,6 +4490,30 @@ func isPrivate(dbChannel *channeldb.OpenChannel) bool { return dbChannel.ChannelFlags&lnwire.FFAnnounceChannel != 1 } +// encodeCustomChanData encodes the custom channel data for the open channel. +// It encodes that data as a pair of var bytes blobs. +func encodeCustomChanData(lnChan *channeldb.OpenChannel) ([]byte, error) { + customOpenChanData := lnChan.CustomBlob.UnwrapOr(nil) + customLocalCommitData := lnChan.LocalCommitment.CustomBlob.UnwrapOr(nil) + + // We'll encode our custom channel data as two blobs. The first is a + // set of var bytes encoding of the open chan data, the second is an + // encoding of the local commitment data. + var customChanDataBuf bytes.Buffer + err := wire.WriteVarBytes(&customChanDataBuf, 0, customOpenChanData) + if err != nil { + return nil, fmt.Errorf("unable to encode open chan "+ + "data: %w", err) + } + err = wire.WriteVarBytes(&customChanDataBuf, 0, customLocalCommitData) + if err != nil { + return nil, fmt.Errorf("unable to encode local commit "+ + "data: %w", err) + } + + return customChanDataBuf.Bytes(), nil +} + // createRPCOpenChannel creates an *lnrpc.Channel from the *channeldb.Channel. func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, isActive, peerAliasLookup bool) (*lnrpc.Channel, error) { @@ -4451,6 +4568,14 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, // is returned and peerScidAlias will be an empty ShortChannelID. peerScidAlias, _ := r.server.aliasMgr.GetPeerAlias(chanID) + // Finally we'll attempt to encode the custom channel data if any + // exists. + customChanBytes, err := encodeCustomChanData(dbChannel) + if err != nil { + return nil, fmt.Errorf("unable to encode open chan data: %w", + err) + } + channel := &lnrpc.Channel{ Active: isActive, Private: isPrivate(dbChannel), @@ -4483,6 +4608,7 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, ZeroConf: dbChannel.IsZeroConf(), ZeroConfConfirmedScid: dbChannel.ZeroConfRealScid().ToUint64(), Memo: string(dbChannel.Memo), + CustomChannelData: customChanBytes, // TODO: remove the following deprecated fields CsvDelay: uint32(dbChannel.LocalChanCfg.CsvDelay), LocalChanReserveSat: int64(dbChannel.LocalChanCfg.ChanReserve), diff --git a/rpcserver_test.go b/rpcserver_test.go index ca70ad9df..9dd3c3f86 100644 --- a/rpcserver_test.go +++ b/rpcserver_test.go @@ -1,14 +1,79 @@ package lnd import ( + "fmt" "testing" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestGetAllPermissions(t *testing.T) { perms := GetAllPermissions() - // Currently there are there are 16 entity:action pairs in use. + // Currently there are 16 entity:action pairs in use. assert.Equal(t, len(perms), 16) } + +// mockDataParser is a mock implementation of the AuxDataParser interface. +type mockDataParser struct { +} + +// InlineParseCustomData replaces any custom data binary blob in the given RPC +// message with its corresponding JSON formatted data. This transforms the +// binary (likely TLV encoded) data to a human-readable JSON representation +// (still as byte slice). +func (m *mockDataParser) InlineParseCustomData(msg proto.Message) error { + switch m := msg.(type) { + case *lnrpc.ChannelBalanceResponse: + m.CustomChannelData = []byte(`{"foo": "bar"}`) + + return nil + + default: + return fmt.Errorf("mock only supports ChannelBalanceResponse") + } +} + +func TestAuxDataParser(t *testing.T) { + // We create an empty channeldb, so we can fetch some channels. + cdb, err := channeldb.Open(t.TempDir()) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, cdb.Close()) + }) + + r := &rpcServer{ + server: &server{ + chanStateDB: cdb.ChannelStateDB(), + implCfg: &ImplementationCfg{ + AuxComponents: AuxComponents{ + AuxDataParser: fn.Some[AuxDataParser]( + &mockDataParser{}, + ), + }, + }, + }, + } + + // With the aux data parser in place, we should get a formatted JSON + // in the custom channel data field. + resp, err := r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, []byte(`{"foo": "bar"}`), resp.CustomChannelData) + + // If we don't supply the aux data parser, we should get the raw binary + // data. Which in this case is just two VarInt fields (1 byte each) that + // represent the value of 0 (zero active and zero pending channels). + r.server.implCfg.AuxComponents.AuxDataParser = fn.None[AuxDataParser]() + + resp, err = r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, []byte{0x00, 0x00}, resp.CustomChannelData) +} diff --git a/server.go b/server.go index 695375400..0cc787028 100644 --- a/server.go +++ b/server.go @@ -1287,6 +1287,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return &pc.Incoming }, AuxLeafStore: implCfg.AuxLeafStore, + AuxSigner: implCfg.AuxSigner, }, dbs.ChanStateDB) // Select the configuration and funding parameters for Bitcoin. @@ -1531,9 +1532,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr, EnableUpfrontShutdown: cfg.EnableUpfrontShutdown, MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), - DeleteAliasEdge: deleteAliasEdge, - AliasManager: s.aliasMgr, - IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint, + DeleteAliasEdge: deleteAliasEdge, + AliasManager: s.aliasMgr, + IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint, + AuxFundingController: implCfg.AuxFundingController, + AuxSigner: implCfg.AuxSigner, }) if err != nil { return nil, err @@ -4089,6 +4092,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, MaxFeeExposure: thresholdMSats, Quit: s.quit, AuxLeafStore: s.implCfg.AuxLeafStore, + AuxSigner: s.implCfg.AuxSigner, MsgRouter: s.implCfg.MsgRouter, }