diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index 1455de1..c824619 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -137,6 +137,12 @@ type batchConfig struct { // the caller has to update it in the source of SweepInfo (interface // SweepFetcher) and re-add the sweep by calling AddSweep. noBumping bool + + // customMuSig2Signer is a custom signer. If it is set, it is used to + // create musig2 signatures instead of musig2SignSweep and signerClient. + // Note that musig2SignSweep must be nil in this case, however signer + // client must still be provided, as it is used for non-coop spendings. + customMuSig2Signer SignMuSig2 } // rbfCache stores data related to our last fee bump. @@ -503,9 +509,12 @@ func (b *batch) Run(ctx context.Context) error { close(b.finished) }() - if b.muSig2SignSweep == nil { + if b.muSig2SignSweep == nil && b.cfg.customMuSig2Signer == nil { return fmt.Errorf("no musig2 signer available") } + if b.muSig2SignSweep != nil && b.cfg.customMuSig2Signer != nil { + return fmt.Errorf("both musig2 signers provided") + } blockChan, blockErrChan, err := b.chainNotifier.RegisterBlockEpochNtfn(runCtx) @@ -1008,6 +1017,36 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep, return nil, fmt.Errorf("invalid htlc script version") } + var digest [32]byte + copy(digest[:], sigHash) + + // If a custom signer is installed, use it instead of b.signerClient + // and b.muSig2SignSweep. + if b.cfg.customMuSig2Signer != nil { + // Produce a signature. + finalSig, err := b.cfg.customMuSig2Signer( + ctx, muSig2Version, sweep.swapHash, + htlcScript.RootHash, digest, + ) + if err != nil { + return nil, fmt.Errorf("customMuSig2Signer failed: %w", + err) + } + + // To be sure that we're good, parse and validate that the + // combined signature is indeed valid for the sig hash and the + // internal pubkey. + err = b.verifySchnorrSig( + htlcScript.TaprootKey, sigHash, finalSig, + ) + if err != nil { + return nil, fmt.Errorf("verifySchnorrSig failed: %w", + err) + } + + return finalSig, nil + } + // Now we're creating a local MuSig2 session using the receiver key's // key locator and the htlc's root hash. keyLocator := &sweep.htlcKeys.ClientScriptKeyLocator @@ -1052,9 +1091,6 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep, "nonces missing") } - var digest [32]byte - copy(digest[:], sigHash) - // Since our MuSig2 session has all nonces, we can now create // the local partial signature by signing the sig hash. _, err = b.signerClient.MuSig2Sign( diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index 799961a..ee01c0f 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" @@ -140,6 +141,12 @@ type MuSig2SignSweep func(ctx context.Context, prevoutMap map[wire.OutPoint]*wire.TxOut) ( []byte, []byte, error) +// SignMuSig2 is a function that can be used to sign a sweep transaction in a +// custom way. +type SignMuSig2 func(ctx context.Context, muSig2Version input.MuSig2Version, + swapHash lntypes.Hash, rootHash chainhash.Hash, sigHash [32]byte, +) ([]byte, error) + // VerifySchnorrSig is a function that can be used to verify a schnorr // signature. type VerifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error @@ -245,6 +252,12 @@ type Batcher struct { // the caller has to update it in the source of SweepInfo (interface // SweepFetcher) and re-add the sweep by calling AddSweep. noBumping bool + + // customMuSig2Signer is a custom signer. If it is set, it is used to + // create musig2 signatures instead of musig2SignSweep and signerClient. + // Note that musig2SignSweep must be nil in this case, however signer + // client must still be provided, as it is used for non-coop spendings. + customMuSig2Signer SignMuSig2 } // BatcherConfig holds batcher configuration. @@ -254,6 +267,12 @@ type BatcherConfig struct { // the caller has to update it in the source of SweepInfo (interface // SweepFetcher) and re-add the sweep by calling AddSweep. noBumping bool + + // customMuSig2Signer is a custom signer. If it is set, it is used to + // create musig2 signatures instead of musig2SignSweep and signerClient. + // Note that musig2SignSweep must be nil in this case, however signer + // client must still be provided, as it is used for non-coop spendings. + customMuSig2Signer SignMuSig2 } // BatcherOption configures batcher behaviour. @@ -269,6 +288,17 @@ func WithNoBumping() BatcherOption { } } +// WithCustomSignMuSig2 instructs sweepbatcher to use a custom function to +// produce MuSig2 signatures. If it is set, it is used to create +// musig2 signatures instead of musig2SignSweep and signerClient. Note +// that musig2SignSweep must be nil in this case, however signerClient +// must still be provided, as it is used for non-coop spendings. +func WithCustomSignMuSig2(customMuSig2Signer SignMuSig2) BatcherOption { + return func(cfg *BatcherConfig) { + cfg.customMuSig2Signer = customMuSig2Signer + } +} + // NewBatcher creates a new Batcher instance. func NewBatcher(wallet lndclient.WalletKitClient, chainNotifier lndclient.ChainNotifierClient, @@ -282,21 +312,27 @@ func NewBatcher(wallet lndclient.WalletKitClient, opt(&cfg) } + if cfg.customMuSig2Signer != nil && musig2ServerSigner != nil { + panic("customMuSig2Signer must not be used with " + + "musig2ServerSigner") + } + return &Batcher{ - batches: make(map[int32]*batch), - sweepReqs: make(chan SweepRequest), - errChan: make(chan error, 1), - quit: make(chan struct{}), - initDone: make(chan struct{}), - wallet: wallet, - chainNotifier: chainNotifier, - signerClient: signerClient, - musig2ServerSign: musig2ServerSigner, - VerifySchnorrSig: verifySchnorrSig, - chainParams: chainparams, - store: store, - sweepStore: sweepStore, - noBumping: cfg.noBumping, + batches: make(map[int32]*batch), + sweepReqs: make(chan SweepRequest), + errChan: make(chan error, 1), + quit: make(chan struct{}), + initDone: make(chan struct{}), + wallet: wallet, + chainNotifier: chainNotifier, + signerClient: signerClient, + musig2ServerSign: musig2ServerSigner, + VerifySchnorrSig: verifySchnorrSig, + chainParams: chainparams, + store: store, + sweepStore: sweepStore, + noBumping: cfg.noBumping, + customMuSig2Signer: cfg.customMuSig2Signer, } } @@ -456,6 +492,7 @@ func (b *Batcher) spinUpBatch(ctx context.Context) (*batch, error) { cfg := batchConfig{ maxTimeoutDistance: defaultMaxTimeoutDistance, noBumping: b.noBumping, + customMuSig2Signer: b.customMuSig2Signer, } switch b.chainParams { @@ -574,6 +611,7 @@ func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error { cfg := batchConfig{ maxTimeoutDistance: batch.cfg.maxTimeoutDistance, noBumping: b.noBumping, + customMuSig2Signer: b.customMuSig2Signer, } newBatch, err := NewBatchFromDB(cfg, batchKit) @@ -637,6 +675,7 @@ func (b *Batcher) FetchUnconfirmedBatches(ctx context.Context) ([]*batch, bchCfg := batchConfig{ maxTimeoutDistance: bch.MaxTimeoutDistance, noBumping: b.noBumping, + customMuSig2Signer: b.customMuSig2Signer, } batch.cfg = &bchCfg diff --git a/sweepbatcher/sweep_batcher_test.go b/sweepbatcher/sweep_batcher_test.go index dadfc6a..f10ede6 100644 --- a/sweepbatcher/sweep_batcher_test.go +++ b/sweepbatcher/sweep_batcher_test.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -68,6 +69,18 @@ func testMuSig2SignSweep(ctx context.Context, return nil, nil, nil } +var customSignature = func() []byte { + sig := [64]byte{10, 20, 30} + return sig[:] +}() + +func testSignMuSig2func(ctx context.Context, muSig2Version input.MuSig2Version, + swapHash lntypes.Hash, rootHash chainhash.Hash, + sigHash [32]byte) ([]byte, error) { + + return customSignature, nil +} + var dummyNotifier = SpendNotifier{ SpendChan: make(chan *SpendDetail, ntfnBufferSize), SpendErrChan: make(chan error, ntfnBufferSize), @@ -1985,6 +1998,89 @@ func testSweepBatcherCloseDuringAdding(t *testing.T, store testStore, <-registrationChan } +// testCustomSignMuSig2 tests the operation with custom musig2 signer. +func testCustomSignMuSig2(t *testing.T, store testStore, + batcherStore testBatcherStore) { + + defer test.Guard(t)() + + lnd := test.NewMockLnd() + ctx, cancel := context.WithCancel(context.Background()) + + sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams) + require.NoError(t, err) + + // Use custom MuSig2 signer function. + batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, + nil, testVerifySchnorrSig, lnd.ChainParams, batcherStore, + sweepStore, WithCustomSignMuSig2(testSignMuSig2func)) + + var wg sync.WaitGroup + wg.Add(1) + + var runErr error + go func() { + defer wg.Done() + runErr = batcher.Run(ctx) + }() + + // Wait for the batcher to be initialized. + <-batcher.initDone + + // Create a sweep request. + sweepReq := SweepRequest{ + SwapHash: lntypes.Hash{1, 1, 1}, + Value: 111, + Outpoint: wire.OutPoint{ + Hash: chainhash.Hash{1, 1}, + Index: 1, + }, + Notifier: &dummyNotifier, + } + + swap := &loopdb.LoopOutContract{ + SwapContract: loopdb.SwapContract{ + CltvExpiry: 111, + AmountRequested: 111, + ProtocolVersion: loopdb.ProtocolVersionMuSig2, + HtlcKeys: loopdb.HtlcKeys{ + SenderScriptKey: senderKey, + ReceiverScriptKey: receiverKey, + SenderInternalPubKey: senderKey, + ReceiverInternalPubKey: receiverKey, + }, + }, + + DestAddr: destAddr, + SwapInvoice: swapInvoice, + SweepConfTarget: 111, + } + + err = store.CreateLoopOut(ctx, sweepReq.SwapHash, swap) + require.NoError(t, err) + store.AssertLoopOutStored() + + // Deliver sweep request to batcher. + require.NoError(t, batcher.AddSweep(&sweepReq)) + + // Since a batch was created we check that it registered for its primary + // sweep's spend. + <-lnd.RegisterSpendChannel + + // Wait for tx to be published. + tx := <-lnd.TxPublishChannel + + // Check the signature. + gotSig := tx.TxIn[0].Witness[0] + require.Equal(t, customSignature, gotSig, "signatures don't match") + + // Now make the batcher quit by canceling the context. + cancel() + wg.Wait() + + checkBatcherError(t, runErr) +} + // TestSweepBatcherBatchCreation tests that sweep requests enter the expected // batch based on their timeout distance. func TestSweepBatcherBatchCreation(t *testing.T) { @@ -2070,6 +2166,11 @@ func TestSweepBatcherCloseDuringAdding(t *testing.T) { runTests(t, testSweepBatcherCloseDuringAdding) } +// TestCustomSignMuSig2 tests the operation with custom musig2 signer. +func TestCustomSignMuSig2(t *testing.T) { + runTests(t, testCustomSignMuSig2) +} + // testBatcherStore is BatcherStore used in tests. type testBatcherStore interface { BatcherStore