From d908e8c2f7e5bbf17549f1ad12b3fb594c663d28 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:05 -0700 Subject: [PATCH 1/3] 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 | 149 +++++++++++++++++++++++++++++++++++++++++ lnwallet/commitment.go | 38 +++++++++++ 2 files changed, 187 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..ef7133bd43 --- /dev/null +++ b/lnwallet/aux_signer.go @@ -0,0 +1,149 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/tlv" +) + +// 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 PaymentDescriptor + + // 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 PaymentDescriptor, 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 + + // Cancel is a channel that should be closed if the caller wishes to + // abandon the job. + Cancel chan struct{} + + // ErrResp is a channel that will be used to send the result of the + // verify job. + ErrResp chan error +} + +// NewAuxVerifyJob creates a new AuxVerifyJob. +func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing, + incoming bool, htlc PaymentDescriptor, 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 *channeldb.OpenChannel, + 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.Option[tlv.Blob], error) + + // 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.Option[tlv.Blob], error) + + // VerifySecondLevelSigs attempts to synchronously verify a batch of aux + // sig jobs. + VerifySecondLevelSigs(chanState *channeldb.OpenChannel, + commitTx *wire.MsgTx, verifyJob []AuxVerifyJob) error +} diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index bdd10555a1..61c09d9832 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -833,6 +833,44 @@ func updateAuxBlob(chanState *channeldb.OpenChannel, ) } +// packSigs is a helper function that attempts to pack a series of aux +// signatures and packs them into a single blob that can be sent alongside the +// CommitSig messages. +func packSigs(auxSigs []fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) (tlv.Blob, error) { + + var result tlv.Blob + mapErr := fn.MapOptionZ(signer, func(s AuxSigner) error { + blobOption, err := s.PackSigs(auxSigs) + if err != nil { + return fmt.Errorf("error packing aux sigs: %w", err) + } + + blobOption.WhenSome(func(blob tlv.Blob) { + result = blob + }) + + return nil + }) + + return result, mapErr +} + +// unpackSigs is a helper function that takes a packed blob of signatures and +// returns the original signatures for each HTLC, keyed by HTLC index. +func unpackSigs(blob fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) ([]fn.Option[tlv.Blob], error) { + + var result []fn.Option[tlv.Blob] + mapErr := fn.MapOptionZ(signer, func(s AuxSigner) error { + var err error + result, err = s.UnpackSigs(blob) + return err + }) + + return result, mapErr +} + // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment From 9b869f95bf35dd0c12069e6f9eb5c9eb480092fc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:26 -0700 Subject: [PATCH 2/3] 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 51bea4ba43..1217adcbef 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1377,6 +1377,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 @@ -1441,6 +1445,7 @@ type channelOpts struct { remoteNonce *musig2.Nonces leafStore fn.Option[AuxLeafStore] + auxSigner fn.Option[AuxSigner] skipNonceInit bool } @@ -1479,6 +1484,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{} @@ -1522,6 +1534,7 @@ func NewLightningChannel(signer input.Signer, lc := &LightningChannel{ Signer: signer, leafStore: opts.leafStore, + auxSigner: opts.auxSigner, sigPool: sigPool, currentHeight: localCommit.CommitHeight, remoteCommitChain: newCommitmentChain(), From ebf9856fac5646ec92d3bfb756752f5b07316964 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:48:36 -0700 Subject: [PATCH 3/3] lnwallet: 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 | 12 +- contractcourt/chain_arbitrator.go | 10 ++ funding/manager.go | 7 ++ htlcswitch/link.go | 2 + lnwallet/channel.go | 182 +++++++++++++++++++++++------- lnwallet/config.go | 4 + lnwallet/sigpool.go | 2 + lnwallet/wallet.go | 3 + peer/brontide.go | 10 ++ server.go | 2 + 11 files changed, 194 insertions(+), 44 deletions(-) diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 934a84996b..8e0142eec7 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 aade3f96e4..28cb05f9ac 100644 --- a/config_builder.go +++ b/config_builder.go @@ -170,10 +170,14 @@ type AuxComponents struct { MsgRouter fn.Option[protofsm.MsgRouter] // AuxFundingController is an optional controller that can be used to - // modify the way we handle certain custom chanenl types. It's also + // modify the way we handle certain custom channel types. It's also // able to automatically handle new custom protocol messages related to // the funding process. AuxFundingController fn.Option[funding.AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // 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, Dialer: func(addr string) (net.Conn, error) { @@ -732,6 +737,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 @@ -911,10 +917,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/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 3245162cb5..d7d9e93cfd 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 b623ce9d98..086fb9c20c 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -548,6 +548,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 @@ -1077,6 +1081,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/htlcswitch/link.go b/htlcswitch/link.go index ab4284d62a..f92fcaadc1 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2143,6 +2143,7 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { CommitSig: msg.CommitSig, HtlcSigs: msg.HtlcSigs, PartialSig: msg.PartialSig, + AuxSigBlob: msg.ExtraData, }) if err != nil { // If we were unable to reconstruct their proposed @@ -2556,6 +2557,7 @@ func (l *channelLink) updateCommitTx() error { CommitSig: newCommit.CommitSig, HtlcSigs: newCommit.HtlcSigs, PartialSig: newCommit.PartialSig, + ExtraData: newCommit.AuxSigBlob, } l.cfg.Peer.SendMessage(false, commitSig) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 1217adcbef..1fbfeee894 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3651,7 +3651,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 @@ -3668,16 +3669,17 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit // smaller (than its total capacity) and some HTLCs may be dust. - numSigs := (len(remoteCommitView.incomingHTLCs) + - len(remoteCommitView.outgoingHTLCs)) + numSigs := len(remoteCommitView.incomingHTLCs) + + len(remoteCommitView.outgoingHTLCs) sigBatch := make([]SignJob, 0, numSigs) + auxSigBatch := make([]AuxSigJob, 0, numSigs) var err error cancelChan := make(chan struct{}) diskCommit, err := remoteCommitView.toDiskCommit(true) if err != nil { - return nil, nil, fmt.Errorf("unable to convert "+ + return nil, nil, nil, fmt.Errorf("unable to convert "+ "commitment: %w", err) } @@ -3685,7 +3687,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanState, *diskCommit, leafStore, *keyRing, ) if err != nil { - return nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ + return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ "%w", err) } @@ -3736,11 +3738,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(auxLeaf), ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - // TODO(roasbeef): hook up signer interface here - // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3767,11 +3767,18 @@ 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() { - //nolint:lll - sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod + sigJob.SignDesc.SignMethod = + input.TaprootScriptSpendSignMethod } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, true, htlc, + remoteCommitView.customBlob, fn.FlattenOption(auxLeaf), + cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { if HtlcIsDust( @@ -3816,7 +3823,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(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 @@ -3845,13 +3852,21 @@ 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 + sigJob.SignDesc.SignMethod = + input.TaprootScriptSpendSignMethod } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, false, htlc, + remoteCommitView.customBlob, fn.FlattenOption(auxLeaf), + cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } - return sigBatch, cancelChan, nil + return sigBatch, auxSigBatch, cancelChan, nil } // createCommitDiff will create a commit diff given a new pending commitment @@ -3860,8 +3875,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, - error) { + commitSig lnwire.Sig, htlcSigs []lnwire.Sig, + auxSigs []fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) { // First, we need to convert the funding outpoint into the ID that's // used on the wire to identify this channel. We'll use this shortly @@ -4007,6 +4022,11 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, "commit: %w", err) } + auxSigBlob, err := packSigs(auxSigs, lc.auxSigner) + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + return &channeldb.CommitDiff{ Commitment: *diskCommit, CommitSig: &lnwire.CommitSig{ @@ -4015,6 +4035,7 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, ), CommitSig: commitSig, HtlcSigs: htlcSigs, + ExtraData: auxSigBlob, }, LogUpdates: logUpdates, OpenedCircuitKeys: openCircuitKeys, @@ -4487,6 +4508,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 @@ -4511,6 +4536,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() @@ -4605,7 +4632,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, ) @@ -4614,6 +4641,16 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } lc.sigPool.SubmitSignBatch(sigBatch) + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.SubmitSecondLevelSigBatch( + 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. @@ -4657,11 +4694,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 @@ -4672,12 +4714,30 @@ 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, err := lc.createCommitDiff(newCommitView, sig, htlcSigs) + commitDiff, err := lc.createCommitDiff( + newCommitView, sig, htlcSigs, auxSigs, + ) if err != nil { return nil, err } @@ -4699,6 +4759,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { CommitSig: sig, HtlcSigs: htlcSigs, PartialSig: lnwire.MaybePartialSigWithNonce(partialSig), + AuxSigBlob: commitDiff.CommitSig.ExtraData, }, PendingHTLCs: commitDiff.Commitment.Htlcs, }, nil @@ -5178,7 +5239,8 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, 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 @@ -5194,13 +5256,14 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // enough capacity to hold verification jobs for all HTLC's in this // view. In the case that we have some dust outputs, then the actual // length will be smaller than the total capacity. - numHtlcs := (len(localCommitmentView.incomingHTLCs) + - len(localCommitmentView.outgoingHTLCs)) + numHtlcs := len(localCommitmentView.incomingHTLCs) + + len(localCommitmentView.outgoingHTLCs) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs) diskCommit, err := localCommitmentView.toDiskCommit(true) if err != nil { - return nil, fmt.Errorf("unable to convert commitment: %w", + return nil, nil, fmt.Errorf("unable to convert commitment: %w", err) } @@ -5208,7 +5271,15 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, chanState, *diskCommit, leafStore, *keyRing, ) if err != nil { - return nil, fmt.Errorf("unable to fetch aux leaves: %w", + 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 := unpackSigs(sigBlob, auxSigner) + if err != nil { + return nil, nil, fmt.Errorf("error unpacking aux sigs: %w", err) } @@ -5222,6 +5293,9 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcIndex uint64 sigHash func() ([]byte, error) sig input.Signature + htlc *PaymentDescriptor + incoming bool + auxLeaf input.AuxTapLeaf err error ) @@ -5231,10 +5305,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{ @@ -5245,20 +5321,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.IncomingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) successTx, err := CreateHtlcSuccessTx( chanType, isLocalInitiator, op, outputAmt, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5301,7 +5377,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") } @@ -5317,15 +5393,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 @@ -5338,21 +5415,21 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.OutgoingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5397,7 +5474,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") } @@ -5413,7 +5490,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 @@ -5429,17 +5506,27 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, SigHash: sigHash, }) + if len(auxHtlcSigs) > i { + auxSig := auxHtlcSigs[i] + auxVerifyJob := NewAuxVerifyJob( + auxSig, *keyRing, incoming, *htlc, + localCommitmentView.customBlob, auxLeaf, + ) + + 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 @@ -5601,6 +5688,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { }), ) + 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. @@ -5608,9 +5700,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 @@ -5759,6 +5852,17 @@ 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( + 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/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/sigpool.go b/lnwallet/sigpool.go index 2424757f93..78e66d9486 100644 --- a/lnwallet/sigpool.go +++ b/lnwallet/sigpool.go @@ -43,6 +43,8 @@ type VerifyJob struct { // HtlcIndex is the index of the HTLC from the PoV of the remote // party's update log. + // + // TODO(roasbeef): remove -- never actually used? HtlcIndex uint64 // Cancel is a channel that should be closed if the caller wishes to diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 51f7372863..afb8b8f033 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2616,6 +2616,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 5e02643289..d86f98b34c 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -364,6 +364,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 @@ -901,6 +905,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..., ) @@ -4036,6 +4043,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/server.go b/server.go index f7b81f2b27..f0cae5aa1a 100644 --- a/server.go +++ b/server.go @@ -1261,6 +1261,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. @@ -3925,6 +3926,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(), Quit: s.quit, AuxLeafStore: s.implCfg.AuxLeafStore, + AuxSigner: s.implCfg.AuxSigner, MsgRouter: s.implCfg.MsgRouter, }