From 116a6430f0781448c69f3245edf878a2a2a0c87c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:25:09 -0700 Subject: [PATCH 01/21] lnwallet: add new AuxFundingDesc struct This struct will house all the information we'll need to do a class of custom channels that relies primarily on adding additional items to the tapscript root of the HTLC/commitment/funding outputs. --- lnwallet/wallet.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 7e455ab485..1838034502 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 From 72beb7955d38cc61637c8916558ebe8bd3127492 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:26:03 -0700 Subject: [PATCH 02/21] lnwallet: use AuxFundingDesc to populate all custom chan info With this commit, we'll now populate all the custom channel information within the OpenChannel and ChannelCommitment structs. --- lnwallet/wallet.go | 57 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 1838034502..d3779bb47e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -239,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 @@ -292,6 +291,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 } @@ -347,6 +350,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 @@ -1501,7 +1508,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. @@ -1509,6 +1517,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) @@ -1542,7 +1561,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 @@ -1556,7 +1575,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 @@ -1899,6 +1918,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, @@ -1906,6 +1937,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 @@ -2332,6 +2364,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, @@ -2340,6 +2384,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 From 84cc9a1f0b850daba088119d7f761ae8ce35cbaf Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:27:20 -0700 Subject: [PATCH 03/21] funding: create new AuxFundingController interface In this commit, we make a new `AuxFundingController` interface capable of processing messages off the wire. In addition, we can use it to abstract away details w.r.t how we obtain a `AuxFundingDesc` for a given channel. We'll now use this whenever we get a channel funding request, to make sure we pass along the custom state that a channel may require. --- funding/aux_funding.go | 51 ++++++++++++++++++++++++++++++++++++++++++ funding/manager.go | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 funding/aux_funding.go diff --git a/funding/aux_funding.go b/funding/aux_funding.go new file mode 100644 index 0000000000..492612145a --- /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 fb36bd9903..3d3ed4c6fd 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -549,6 +549,12 @@ 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] } // Manager acts as an orchestrator/bridge between the wallet's @@ -1626,6 +1632,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 +1665,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -4634,6 +4658,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 +4714,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) From 65f54cb0752a7472e4d8f84178694c275041d334 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:48:11 -0700 Subject: [PATCH 04/21] config+serer: add AuxFundingController as top level cfg option --- config_builder.go | 7 +++++++ server.go | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config_builder.go b/config_builder.go index 21b1ccee0b..d7625839ee 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,12 @@ 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] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/server.go b/server.go index 0f50794e74..b754155fa3 100644 --- a/server.go +++ b/server.go @@ -1530,9 +1530,10 @@ 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, }) if err != nil { return nil, err From 7144a1c733a370a5ce89647d2d42108b95e07823 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:25:22 -0700 Subject: [PATCH 05/21] lnwallet: add TaprootInternalKey method to ShimIntent If this is a taproot channel, then we'll return the internal key which'll be useful to callers. --- lnwallet/chanfunding/canned_assembler.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 177cc35c38..b3457f21bf 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. From bed4562584737a650c77544ca37da748b72c7877 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:25:57 -0700 Subject: [PATCH 06/21] lnwallet: for PsbtIntent return the internal key in the POutput We also add a new assertion to the itests to ensure the field is being properly set. --- itest/lnd_psbt_test.go | 11 +++++++++++ lnwallet/chanfunding/psbt_assembler.go | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index ac39bb4971..bedc61722b 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/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 10bcd70159..d37f1b7347 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -6,11 +6,13 @@ 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/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) @@ -208,7 +210,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 } From 7ec48a5054ac5fbc5ad852fe4aa91d51a0e49701 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 17 Apr 2024 18:43:01 -0700 Subject: [PATCH 07/21] funding+lnwallet: only blind tapscript root early in funding flow In this commit, we modify the aux funding work flow slightly. We won't be able to generate the full AuxFundingDesc until both sides has sent+received funding params. So we'll now only attempt to bind the tapscript root as soon as we send+recv the open_channel message. We'll now also make sure that we pass the tapscript root all the way down into the musig2 session creation. --- lnwallet/chanfunding/psbt_assembler.go | 8 ++++++ lnwallet/wallet.go | 40 ++++++++++++-------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index d37f1b7347..f678f520fc 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -10,6 +10,7 @@ import ( "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" @@ -164,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 diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index d3779bb47e..554f21db98 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -278,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. @@ -457,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. @@ -503,7 +500,6 @@ func (l *LightningWallet) Startup() error { } l.wg.Add(1) - // TODO(roasbeef): multiple request handlers? go l.requestHandler() return nil @@ -1459,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() @@ -1484,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 @@ -1668,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, @@ -1750,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, }) } @@ -1809,6 +1808,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, musigSessions := genMusigSession( ourContribution, theirContribution, l.Cfg.Signer, fundingOutput, + pendingReservation.partialState.TapscriptRoot, ) pendingReservation.musigSessions = musigSessions } @@ -2198,6 +2198,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, res.musigSessions = genMusigSession( res.ourContribution, res.theirContribution, l.Cfg.Signer, fundingOutput, + res.partialState.TapscriptRoot, ) } @@ -2288,9 +2289,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 From bcb66585d4f828b2cf182585975c9456f9a5c306 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 17 Apr 2024 23:35:44 -0700 Subject: [PATCH 08/21] funding+lnwallet: finish hook up new aux funding flow For the initiator, once we get the signal that the PSBT has been finalized, we'll call into the aux funder to get the funding desc. For the responder, once we receive the funding_created message, we'll do the same. We now also have local+remote aux leaves for the commitment transaction. Some old TODO comments that in retrospect aren't required anymore are removed as well. --- funding/manager.go | 64 +++++++++++++++++----- lnwallet/reservation.go | 96 ++++++++++++++++++++++++++++++--- lnwallet/test/test_interface.go | 2 + lnwallet/wallet.go | 38 ++++++++++++- 4 files changed, 181 insertions(+), 19 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index 3d3ed4c6fd..cb1ec04b93 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 @@ -1262,8 +1261,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. @@ -2285,10 +2284,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 @@ -2414,7 +2437,6 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, // final funding transaction, as well as a signature for our version of // the commitment transaction. So at this point, we can validate the // initiator's commitment transaction, then send our own if it's valid. - // TODO(roasbeef): make case (p vs P) consistent throughout fundingOut := msg.FundingPoint log.Infof("completing pending_id(%x) with ChannelPoint(%v)", pendingChanID[:], fundingOut) @@ -2446,16 +2468,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 @@ -2766,9 +2810,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{ @@ -3670,7 +3711,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 @@ -4468,7 +4509,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 } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 1f0000e8e4..7f1a89c228 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/test/test_interface.go b/lnwallet/test/test_interface.go index bcb396cafa..4a02f0324a 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/wallet.go b/lnwallet/wallet.go index 554f21db98..d72a09da13 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1844,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 @@ -1902,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 @@ -2349,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) From 5c854a2f53fbc661f1505f5a8b50cc5f8d02e0fa Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 17 May 2024 12:39:12 +0200 Subject: [PATCH 09/21] multi: add tapscript root to gossip message --- channeldb/models/channel_edge_info.go | 6 ++++++ discovery/gossiper.go | 19 ++++++++++++++++--- funding/manager.go | 7 ++++--- graph/builder.go | 8 ++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/channeldb/models/channel_edge_info.go b/channeldb/models/channel_edge_info.go index 1afa2d6272..0f91e2bbec 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/discovery/gossiper.go b/discovery/gossiper.go index 84fae767f0..1d3051de57 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 @@ -2578,6 +2588,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/manager.go b/funding/manager.go index cb1ec04b93..7c8309e939 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -3515,6 +3515,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: @@ -4441,9 +4442,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) diff --git a/graph/builder.go b/graph/builder.go index 717d3e5ad8..3c882058bf 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -1092,8 +1092,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( @@ -1140,7 +1140,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 @@ -1272,7 +1272,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 From 0b64b806421c620278d2e598e652cb2d2cc3300a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 17 May 2024 12:40:22 +0200 Subject: [PATCH 10/21] funding: inform aux controller about channel ready/finalize --- funding/manager.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/funding/manager.go b/funding/manager.go index 7c8309e939..2ae0bbed01 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1921,6 +1921,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{ @@ -1981,6 +1983,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[:]) @@ -2406,6 +2412,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) @@ -2441,6 +2449,10 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, 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) @@ -2700,6 +2712,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. @@ -2795,6 +2815,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 @@ -4043,6 +4078,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 @@ -4129,6 +4184,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{ @@ -4907,6 +4975,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, From aa0c680e18f8f17cc03051a3271d7f474a93d85a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:05 -0700 Subject: [PATCH 11/21] lnwallet: add new AuxSigner interface to mirror SigPool In this commit, we add a new aux signer interface that's meant to mirror the SigPool. If present, this'll be used to (maybe) obtain signatures for second level HTLCs for certain classes of custom channels. --- lnwallet/aux_signer.go | 245 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 lnwallet/aux_signer.go diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go new file mode 100644 index 0000000000..a724f20cbe --- /dev/null +++ b/lnwallet/aux_signer.go @@ -0,0 +1,245 @@ +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" +) + +// 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. + Resp chan AuxSigJobResp + + // 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{} +} + +// 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 +} From 953fb073d4a403177e4f8923f952bd8fa153facb Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Sep 2024 15:47:33 +0200 Subject: [PATCH 12/21] lnwallet: allow read-only access to HtlcView's HTLCs Due to a recent refactor, the HTLCs are no longer an exported type. Custom channels need access to those updates, so we provide them in a read-only manner. --- lnwallet/channel.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 32492171ff..a208cda930 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2545,6 +2545,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. From f52a16333468f8e630a2b8560a5c281368af4517 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 11 Sep 2024 15:28:46 +0200 Subject: [PATCH 13/21] lnwallet: clarify usage of cancel and response channels --- lnwallet/aux_signer.go | 11 ++++++----- lnwallet/sigpool.go | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go index a724f20cbe..6ce2e99da8 100644 --- a/lnwallet/aux_signer.go +++ b/lnwallet/aux_signer.go @@ -151,18 +151,19 @@ type AuxSigJob struct { BaseAuxJob // Resp is a channel that will be used to send the result of the sign - // job. + // job. This channel MUST be buffered. Resp chan AuxSigJobResp - // 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{} } // 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 { + htlcLeaf input.AuxTapLeaf, cancelChan <-chan struct{}) AuxSigJob { return AuxSigJob{ SignDesc: sigJob.SignDesc, diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go index 2424757f93..2296e17031 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 From 1e85c5054e0c160c2adcba4bbe357d5fa0772ea6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:26 -0700 Subject: [PATCH 14/21] lnwallet: add WithAuxSigner option to channel --- lnwallet/channel.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index a208cda930..3f81000a25 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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, From bd84fd256e46c409aa2f679437aa6602c11db377 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 2 Sep 2024 14:33:49 +0200 Subject: [PATCH 15/21] lnwire: add custom records field to type `CommitSig` --- lnwire/commit_sig.go | 38 +++++---- lnwire/commit_sig_test.go | 168 ++++++++++++++++++++++++++++++++++++++ lnwire/lnwire_test.go | 2 + 3 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 lnwire/commit_sig_test.go diff --git a/lnwire/commit_sig.go b/lnwire/commit_sig.go index 7deb64ae1c..3a475e71ff 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 0000000000..0772a2fb83 --- /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 7eb434f454..e941962d57 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -945,6 +945,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) From 83fdbda2fa9f9c2fec54855d34ea7eb65911d571 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:48:36 -0700 Subject: [PATCH 16/21] multi: obtain+verify aux sigs for all second level HTLCs In this commit, we start to use the new AuxSigner to obtain+verify aux sigs for all second level HTLCs. This is similar to the existing SigPool, but we'll only attempt to do this if the AuxSigner is present (won't be for most channels). --- chainreg/chainregistry.go | 4 + config_builder.go | 10 +- contractcourt/breach_arbitrator_test.go | 5 + contractcourt/chain_arbitrator.go | 10 + funding/manager.go | 7 + funding/manager_test.go | 4 + htlcswitch/link.go | 24 ++- htlcswitch/link_test.go | 6 + htlcswitch/test_utils.go | 9 + lnwallet/aux_signer.go | 4 + lnwallet/channel.go | 264 ++++++++++++++++++++---- lnwallet/channel_test.go | 178 +++++++++++++++- lnwallet/config.go | 4 + lnwallet/mock.go | 52 +++++ lnwallet/test_utils.go | 44 ++++ lnwallet/transactions_test.go | 5 + lnwallet/wallet.go | 3 + peer/brontide.go | 10 + peer/test_utils.go | 4 + server.go | 3 + 20 files changed, 597 insertions(+), 53 deletions(-) diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 41a2fcbb7f..da1f8e08ad 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/config_builder.go b/config_builder.go index d7625839ee..ca32ac4cda 100644 --- a/config_builder.go +++ b/config_builder.go @@ -174,6 +174,10 @@ type AuxComponents struct { // 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] } // DefaultWalletImpl is the default implementation of our normal, btcwallet @@ -580,6 +584,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{ @@ -737,6 +742,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 @@ -919,10 +925,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 5940ee25bd..cf575f1534 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 dbc97939a9..d61e479018 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/funding/manager.go b/funding/manager.go index 2ae0bbed01..ed0ef0e7fc 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -554,6 +554,10 @@ type Config struct { // 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 @@ -1083,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( diff --git a/funding/manager_test.go b/funding/manager_test.go index d1aa9ec146..898d2c3629 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/htlcswitch/link.go b/htlcswitch/link.go index 83495f357b..c92c431712 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2191,10 +2191,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 @@ -2621,11 +2631,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) diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 53a084209e..02fbd3ba0e 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 a71577ef2b..1786765fb5 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -351,8 +351,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 @@ -362,6 +365,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 @@ -423,6 +428,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", @@ -469,6 +476,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/lnwallet/aux_signer.go b/lnwallet/aux_signer.go index 6ce2e99da8..5d4bc79241 100644 --- a/lnwallet/aux_signer.go +++ b/lnwallet/aux_signer.go @@ -9,6 +9,10 @@ import ( "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 { diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 3f81000a25..b344b543b6 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3089,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 @@ -3109,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{}) @@ -3123,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 @@ -3173,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] @@ -3210,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( @@ -3253,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 @@ -3282,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 @@ -3297,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 @@ -3366,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 @@ -3773,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 @@ -3797,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() @@ -3889,7 +3956,7 @@ 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, ) @@ -3898,6 +3965,17 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } 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. @@ -3941,11 +4019,16 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { sort.Slice(sigBatch, func(i, j int) bool { return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex }) + sort.Slice(auxSigBatch, func(i, j int) bool { + return auxSigBatch[i].OutputIndex < auxSigBatch[j].OutputIndex + }) // With the jobs sorted, we'll now iterate through all the responses to // gather each of the signatures in order. 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 @@ -3956,12 +4039,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 @@ -3975,11 +4080,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 @@ -3991,8 +4103,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) @@ -4197,13 +4309,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) @@ -4496,7 +4618,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 @@ -4515,6 +4638,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( @@ -4526,7 +4650,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, @@ -4539,6 +4676,9 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcIndex uint64 sigHash func() ([]byte, error) sig input.Signature + htlc *paymentDescriptor + incoming bool + auxLeaf input.AuxTapLeaf err error ) @@ -4548,10 +4688,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{ @@ -4617,7 +4759,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") } @@ -4633,15 +4775,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 @@ -4712,7 +4855,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") } @@ -4728,7 +4871,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 @@ -4744,17 +4887,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 @@ -4913,6 +5079,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. @@ -4920,9 +5091,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 @@ -5075,6 +5247,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 diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 744be3ae5a..c47f877105 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 24961f38ed..425fe15dad 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 2afff4f215..82b9e19c24 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/test_utils.go b/lnwallet/test_utils.go index a9f71f24c1..d4f0d05aef 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 3588acfebf..8786c2d5dc 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 d72a09da13..f60856113e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2607,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/peer/brontide.go b/peer/brontide.go index 3223e7f4b7..2e4646a78f 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 e0ae29be8b..948a22c4e3 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/server.go b/server.go index b754155fa3..917558f3ac 100644 --- a/server.go +++ b/server.go @@ -1286,6 +1286,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. @@ -1534,6 +1535,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, AliasManager: s.aliasMgr, IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint, AuxFundingController: implCfg.AuxFundingController, + AuxSigner: implCfg.AuxSigner, }) if err != nil { return nil, err @@ -4089,6 +4091,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, } From ea83300942c994073252f311858b02a311acba06 Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Sun, 8 Sep 2024 17:00:13 -0400 Subject: [PATCH 17/21] lnwallet: sort sig jobs before submission To make sure we attempt to read the results of the sig batches in the same order they're processed, we sort them _before_ submitting them to the batch processor. Otherwise it might happen that we try to read on a result channel that was never sent on because we aborted due to an error. We also use slices.SortFunc now which doesn't use reflection and might be slightly faster. --- lnwallet/channel.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b344b543b6..0bd1938a7e 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" @@ -3963,6 +3963,17 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { 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 { @@ -4013,18 +4024,8 @@ 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 - }) - sort.Slice(auxSigBatch, func(i, j int) bool { - return auxSigBatch[i].OutputIndex < auxSigBatch[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)) auxSigs := make([]fn.Option[tlv.Blob], 0, len(auxSigBatch)) for i := range sigBatch { From 5e1a98cd43dc308d2e70caa72bca9484de78ece6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 17 May 2024 15:19:05 +0200 Subject: [PATCH 18/21] lnrpc+rpcserver: add and populate custom channel data --- lnrpc/lightning.pb.go | 49 ++++++++++++++++++++++---- lnrpc/lightning.proto | 16 +++++++++ lnrpc/lightning.swagger.json | 15 ++++++++ lntest/harness_assertion.go | 5 +++ rpcserver.go | 68 ++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 6 deletions(-) diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 8ff18e4b11..26ab31dd93 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 6d975c8c3e..b44b04caa0 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 fe204e12c2..0e69fe1fdf 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/lntest/harness_assertion.go b/lntest/harness_assertion.go index 9538c48e71..add5b91f92 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/rpcserver.go b/rpcserver.go index d46e04741e..cbd4102bee 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3529,6 +3529,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 +3537,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 +3556,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 +3570,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 "+ @@ -3594,6 +3621,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, Sat: uint64(pendingOpenRemoteBalance.ToSatoshis()), Msat: uint64(pendingOpenRemoteBalance), }, + CustomChannelData: customDataBuf.Bytes(), // Deprecated fields. Balance: int64(localBalance.ToSatoshis()), @@ -3654,6 +3682,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 +3701,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), @@ -4397,6 +4432,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 +4510,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 +4550,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), From d49da574e36d775e96fca50d1dc2e631d2a78450 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 17 May 2024 15:19:28 +0200 Subject: [PATCH 19/21] lnd: add aux data parser This commit adds an optional data parser that can inspect and in-place format custom data of certain RPC messages. We don't add an implementation of the interface itself, as that will be provided by external components when packaging up lnd as a bundle with other software. --- config_builder.go | 4 ++ lnrpc/routerrpc/router_backend.go | 15 ++++++- rpcserver.go | 62 +++++++++++++++++++++++++++- rpcserver_test.go | 67 ++++++++++++++++++++++++++++++- 4 files changed, 143 insertions(+), 5 deletions(-) diff --git a/config_builder.go b/config_builder.go index ca32ac4cda..ee4064035f 100644 --- a/config_builder.go +++ b/config_builder.go @@ -178,6 +178,10 @@ type AuxComponents struct { // 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 diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 6694e8b4f6..4ef60a71da 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/rpcserver.go b/rpcserver.go index cbd4102bee..3b8a9ea009 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 { @@ -3596,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), @@ -3626,7 +3652,19 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, // 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 ( @@ -4068,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 } @@ -4382,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 } diff --git a/rpcserver_test.go b/rpcserver_test.go index ca70ad9df7..9dd3c3f86f 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) +} From cdc3a4a6c6e8095359688236319b64f783862430 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 27 May 2024 13:36:01 +0200 Subject: [PATCH 20/21] channeldb: add NextHeight, fix formatting --- lnwallet/channel.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 0bd1938a7e..c78742d139 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -4544,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 @@ -5992,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{ From 52e50d807d928acf10017d70ab13d566d5b84d9b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:26:32 +0200 Subject: [PATCH 21/21] htlcswitch: override amount check on custom records --- htlcswitch/link.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index c92c431712..7f0b3eb8cc 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -3793,7 +3793,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)