diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 41a2fcbb7..da1f8e08a 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -68,6 +68,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // BlockCache is the main cache for storing block information. BlockCache *blockcache.BlockCache diff --git a/config_builder.go b/config_builder.go index d7625839e..ca32ac4cd 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 5940ee25b..cf575f153 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -2360,9 +2360,12 @@ func createInitChannels(t *testing.T) ( ) bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil) + signerMock := lnwallet.NewDefaultAuxSignerMock(t) alicePool := lnwallet.NewSigPool(1, aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err @@ -2375,6 +2378,8 @@ func createInitChannels(t *testing.T) ( bobPool := lnwallet.NewSigPool(1, bobSigner) channelBob, err := lnwallet.NewLightningChannel( bobSigner, bobChannelState, bobPool, + lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}), + lnwallet.WithAuxSigner(signerMock), ) if err != nil { return nil, nil, err diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index dbc97939a..d61e47901 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -221,6 +221,10 @@ type ChainArbitratorConfig struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions, a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) chanMachine, err := lnwallet.NewLightningChannel( a.c.cfg.Signer, channel, nil, chanOpts..., @@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // Finally, we'll force close the channel completing // the force close workflow. diff --git a/funding/manager.go b/funding/manager.go index 2ae0bbed0..ed0ef0e7f 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 d1aa9ec14..898d2c362 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 83495f357..c92c43171 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 53a084209..02fbd3ba0 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 a71577ef2..1786765fb 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 6ce2e99da..5d4bc7924 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 3f81000a2..b344b543b 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 744be3ae5..c47f87710 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -702,6 +703,68 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) { require.NoError(t, err, "unable to receive bob's commitment") } +// TestCommitHTLCSigCustomRecordSize asserts that custom records produced for +// a commitment_signed message are properly limited in size. +func TestCommitHTLCSigCustomRecordSize(t *testing.T) { + aliceChannel, bobChannel, err := CreateTestChannels( + t, channeldb.SimpleTaprootFeatureBit| + channeldb.TapscriptRootBit, + ) + require.NoError(t, err, "unable to create test channels") + + const ( + htlcAmt = lnwire.MilliSatoshi(20000000) + numHtlcs = 2 + ) + + largeRecords := lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType: bytes.Repeat([]byte{0}, 65_500), + } + largeBlob, err := largeRecords.Serialize() + require.NoError(t, err) + + aliceChannel.auxSigner.WhenSome(func(a AuxSigner) { + mockSigner, ok := a.(*MockAuxSigner) + require.True(t, ok, "expected MockAuxSigner") + + // Replace the default PackSigs implementation to return a + // large custom records blob. + mockSigner.ExpectedCalls = fn.Filter(func(c *mock.Call) bool { + return c.Method != "PackSigs" + }, mockSigner.ExpectedCalls) + mockSigner.On("PackSigs", mock.Anything). + Return(fn.Ok(fn.Some(largeBlob))) + }) + + // Add HTLCs with identical payment hashes and amounts, but descending + // CLTV values. We will expect the signatures to appear in the reverse + // order that the HTLCs are added due to the commitment sorting. + for i := 0; i < numHtlcs; i++ { + var ( + preimage lntypes.Preimage + hash = preimage.Hash() + ) + + htlc := &lnwire.UpdateAddHTLC{ + ID: uint64(i), + PaymentHash: hash, + Amount: htlcAmt, + Expiry: uint32(numHtlcs - i), + } + + if _, err := aliceChannel.AddHTLC(htlc, nil); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + t.Fatalf("bob unable to receive htlc: %v", err) + } + } + + // We expect an error because of the large custom records blob. + _, err = aliceChannel.SignNextCommitment() + require.ErrorContains(t, err, "exceeds max allowed size") +} + // TestCooperativeChannelClosure checks that the coop close process finishes // with an agreement from both parties, and that the final balances of the // close tx check out. @@ -3046,6 +3109,10 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) { return channelNew, nil } +// testChanSyncOweCommitment tests that if Bob restarts (and then Alice) before +// he receives Alice's CommitSig message, then Alice concludes that she needs +// to re-send the CommitDiff. After the diff has been sent, both nodes should +// resynchronize and be able to complete the dangling commit. func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) { // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, @@ -3210,8 +3277,10 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) { len(commitSigMsg.HtlcSigs)) } for i, htlcSig := range commitSigMsg.HtlcSigs { - if !bytes.Equal(htlcSig.RawBytes(), - aliceNewCommit.HtlcSigs[i].RawBytes()) { + if !bytes.Equal( + htlcSig.RawBytes(), + aliceNewCommit.HtlcSigs[i].RawBytes(), + ) { t.Fatalf("htlc sig msgs don't match: "+ "expected %v got %v", @@ -3389,6 +3458,100 @@ func TestChanSyncOweCommitment(t *testing.T) { } } +type testSigBlob struct { + BlobInt tlv.RecordT[tlv.TlvType65634, uint16] +} + +// TestChanSyncOweCommitmentAuxSigner tests that when one party owes a +// signature after a channel reest, if an aux signer is present, then the +// signature message sent includes the additional aux sigs as extra data. +func TestChanSyncOweCommitmentAuxSigner(t *testing.T) { + t.Parallel() + + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + chanType := channeldb.SingleFunderTweaklessBit | + channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit | + channeldb.TapscriptRootBit + + aliceChannel, bobChannel, err := CreateTestChannels(t, chanType) + require.NoError(t, err, "unable to create test channels") + + // We'll now manually attach an aux signer to Alice's channel. + auxSigner := &MockAuxSigner{} + aliceChannel.auxSigner = fn.Some[AuxSigner](auxSigner) + + var fakeOnionBlob [lnwire.OnionPacketSize]byte + copy( + fakeOnionBlob[:], + bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize), + ) + + // To kick things off, we'll have Alice send a single HTLC to Bob. + htlcAmt := lnwire.NewMSatFromSatoshis(20000) + var bobPreimage [32]byte + copy(bobPreimage[:], bytes.Repeat([]byte{0}, 32)) + rHash := sha256.Sum256(bobPreimage[:]) + h := &lnwire.UpdateAddHTLC{ + PaymentHash: rHash, + Amount: htlcAmt, + Expiry: uint32(10), + OnionBlob: fakeOnionBlob, + } + + _, err = aliceChannel.AddHTLC(h, nil) + require.NoError(t, err, "unable to recv bob's htlc: %v", err) + + // We'll set up the mock to expect calls to PackSigs and also + // SubmitSubmitSecondLevelSigBatch. + var sigBlobBuf bytes.Buffer + sigBlob := testSigBlob{ + BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5), + } + tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record()) + require.NoError(t, err, "unable to create tlv stream") + require.NoError(t, tlvStream.Encode(&sigBlobBuf)) + + auxSigner.On( + "SubmitSecondLevelSigBatch", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil).Twice() + auxSigner.On( + "PackSigs", mock.Anything, + ).Return( + fn.Ok(fn.Some(sigBlobBuf.Bytes())), nil, + ) + + _, err = aliceChannel.SignNextCommitment() + require.NoError(t, err, "unable to sign commitment") + + _, err = aliceChannel.GenMusigNonces() + require.NoError(t, err, "unable to generate musig nonces") + + // Next we'll simulate a restart, by having Bob send over a chan sync + // message to Alice. + bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg() + require.NoError(t, err, "unable to produce chan sync msg") + + aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg( + bobSyncMsg, + ) + require.NoError(t, err) + require.Len(t, aliceMsgsToSend, 2) + + // The first message should be an update add HTLC. + require.IsType(t, &lnwire.UpdateAddHTLC{}, aliceMsgsToSend[0]) + + // The second should be a commit sig message. + sigMsg, ok := aliceMsgsToSend[1].(*lnwire.CommitSig) + require.True(t, ok) + require.True(t, sigMsg.PartialSig.IsSome()) + + // The signature should have the CustomRecords field set. + require.NotEmpty(t, sigMsg.CustomRecords) +} + func testChanSyncOweCommitmentPendingRemote(t *testing.T, chanType channeldb.ChannelType) { @@ -3398,7 +3561,10 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T, require.NoError(t, err, "unable to create test channels") var fakeOnionBlob [lnwire.OnionPacketSize]byte - copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize)) + copy( + fakeOnionBlob[:], + bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize), + ) // We'll start off the scenario where Bob send two htlcs to Alice in a // single state update. @@ -3437,7 +3603,9 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T, // Next, Alice settles the HTLCs from Bob in distinct state updates. for i := 0; i < numHtlcs; i++ { - err = aliceChannel.SettleHTLC(preimages[i], uint64(i), nil, nil, nil) + err = aliceChannel.SettleHTLC( + preimages[i], uint64(i), nil, nil, nil, + ) if err != nil { t.Fatalf("unable to settle htlc: %v", err) } @@ -3727,7 +3895,7 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) { } // TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before -// he receiver's Alice's RevokeAndAck message, then Alice concludes that she +// he received Alice's RevokeAndAck message, then Alice concludes that she // needs to re-send the RevokeAndAck. After the revocation has been sent, both // nodes should be able to successfully complete another state transition. func TestChanSyncOweRevocation(t *testing.T) { diff --git a/lnwallet/config.go b/lnwallet/config.go index 24961f38e..425fe15da 100644 --- a/lnwallet/config.go +++ b/lnwallet/config.go @@ -67,4 +67,8 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[AuxSigner] } diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 2afff4f21..82b9e19c2 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -21,6 +21,7 @@ import ( "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" ) var ( @@ -441,3 +442,54 @@ func (*MockAuxLeafStore) ApplyHtlcView( return fn.Ok(fn.None[tlv.Blob]()) } + +// MockAuxSigner is a mock implementation of the AuxSigner interface. +type MockAuxSigner struct { + mock.Mock +} + +// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and +// processes them asynchronously. +func (a *MockAuxSigner) SubmitSecondLevelSigBatch(chanState AuxChanState, + tx *wire.MsgTx, jobs []AuxSigJob) error { + + args := a.Called(chanState, tx, jobs) + + // While we return, we'll also send back an instant response for the + // set of jobs. + for _, sigJob := range jobs { + sigJob.Resp <- AuxSigJobResp{} + } + + return args.Error(0) +} + +// PackSigs takes a series of aux signatures and packs them into a +// single blob that can be sent alongside the CommitSig messages. +func (a *MockAuxSigner) PackSigs( + sigs []fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]] { + + args := a.Called(sigs) + + return args.Get(0).(fn.Result[fn.Option[tlv.Blob]]) +} + +// UnpackSigs takes a packed blob of signatures and returns the +// original signatures for each HTLC, keyed by HTLC index. +func (a *MockAuxSigner) UnpackSigs( + sigs fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]] { + + args := a.Called(sigs) + + return args.Get(0).(fn.Result[[]fn.Option[tlv.Blob]]) +} + +// VerifySecondLevelSigs attempts to synchronously verify a batch of aux +// sig jobs. +func (a *MockAuxSigner) VerifySecondLevelSigs(chanState AuxChanState, + tx *wire.MsgTx, jobs []AuxVerifyJob) error { + + args := a.Called(chanState, tx, jobs) + + return args.Error(0) +} diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index a9f71f24c..d4f0d05ae 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -1,6 +1,7 @@ package lnwallet import ( + "bytes" "crypto/rand" "encoding/binary" "encoding/hex" @@ -21,6 +22,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -369,9 +372,13 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, // TODO(roasbeef): make mock version of pre-image store + auxSigner := NewDefaultAuxSignerMock(t) + alicePool := NewSigPool(1, aliceSigner) channelAlice, err := NewLightningChannel( aliceSigner, aliceChannelState, alicePool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) if err != nil { return nil, nil, err @@ -386,6 +393,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, bobPool := NewSigPool(1, bobSigner) channelBob, err := NewLightningChannel( bobSigner, bobChannelState, bobPool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) if err != nil { return nil, nil, err @@ -586,3 +595,38 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { return nil } + +func NewDefaultAuxSignerMock(t *testing.T) *MockAuxSigner { + auxSigner := &MockAuxSigner{} + + type testSigBlob struct { + BlobInt tlv.RecordT[tlv.TlvType65634, uint16] + } + + var sigBlobBuf bytes.Buffer + sigBlob := testSigBlob{ + BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5), + } + tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record()) + require.NoError(t, err, "unable to create tlv stream") + require.NoError(t, tlvStream.Encode(&sigBlobBuf)) + + auxSigner.On( + "SubmitSecondLevelSigBatch", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil) + auxSigner.On( + "PackSigs", mock.Anything, + ).Return(fn.Ok(fn.Some(sigBlobBuf.Bytes()))) + auxSigner.On( + "UnpackSigs", mock.Anything, + ).Return(fn.Ok([]fn.Option[tlv.Blob]{ + fn.Some(sigBlobBuf.Bytes()), + })) + auxSigner.On( + "VerifySecondLevelSigs", mock.Anything, mock.Anything, + mock.Anything, + ).Return(nil) + + return auxSigner +} diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 3588acfeb..8786c2d5d 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -1018,9 +1018,12 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2, }, nil) + auxSigner := NewDefaultAuxSignerMock(t) remotePool := NewSigPool(1, remoteSigner) channelRemote, err := NewLightningChannel( remoteSigner, remoteChannelState, remotePool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) require.NoError(t, err) require.NoError(t, remotePool.Start()) @@ -1028,6 +1031,8 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp localPool := NewSigPool(1, localSigner) channelLocal, err := NewLightningChannel( localSigner, localChannelState, localPool, + WithLeafStore(&MockAuxLeafStore{}), + WithAuxSigner(auxSigner), ) require.NoError(t, err) require.NoError(t, localPool.Start()) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index d72a09da1..f60856113 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 3223e7f4b..2e4646a78 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 e0ae29be8..948a22c4e 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 b754155fa..917558f3a 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, }