Skip to content

Commit aa6d748

Browse files
committed
sweepbatcher: add option WithCustomSignMuSig2
It is needed to provide a custom MuSig2 signer.
1 parent 38e86a1 commit aa6d748

File tree

3 files changed

+194
-18
lines changed

3 files changed

+194
-18
lines changed

sweepbatcher/sweep_batch.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ type batchConfig struct {
137137
// the caller has to update it in the source of SweepInfo (interface
138138
// SweepFetcher) and re-add the sweep by calling AddSweep.
139139
noBumping bool
140+
141+
// customMuSig2Signer is a custom signer. If it is set, it is used to
142+
// create musig2 signatures instead of musig2SignSweep and signerClient.
143+
// Note that musig2SignSweep must be nil in this case, however signer
144+
// client must still be provided, as it is used for non-coop spendings.
145+
customMuSig2Signer SignMuSig2
140146
}
141147

142148
// rbfCache stores data related to our last fee bump.
@@ -503,9 +509,12 @@ func (b *batch) Run(ctx context.Context) error {
503509
close(b.finished)
504510
}()
505511

506-
if b.muSig2SignSweep == nil {
512+
if b.muSig2SignSweep == nil && b.cfg.customMuSig2Signer == nil {
507513
return fmt.Errorf("no musig2 signer available")
508514
}
515+
if b.muSig2SignSweep != nil && b.cfg.customMuSig2Signer != nil {
516+
return fmt.Errorf("both musig2 signers provided")
517+
}
509518

510519
blockChan, blockErrChan, err :=
511520
b.chainNotifier.RegisterBlockEpochNtfn(runCtx)
@@ -1008,6 +1017,36 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
10081017
return nil, fmt.Errorf("invalid htlc script version")
10091018
}
10101019

1020+
var digest [32]byte
1021+
copy(digest[:], sigHash)
1022+
1023+
// If a custom signer is installed, use it instead of b.signerClient
1024+
// and b.muSig2SignSweep.
1025+
if b.cfg.customMuSig2Signer != nil {
1026+
// Produce a signature.
1027+
finalSig, err := b.cfg.customMuSig2Signer(
1028+
ctx, muSig2Version, sweep.swapHash,
1029+
htlcScript.RootHash, digest,
1030+
)
1031+
if err != nil {
1032+
return nil, fmt.Errorf("customMuSig2Signer failed: %w",
1033+
err)
1034+
}
1035+
1036+
// To be sure that we're good, parse and validate that the
1037+
// combined signature is indeed valid for the sig hash and the
1038+
// internal pubkey.
1039+
err = b.verifySchnorrSig(
1040+
htlcScript.TaprootKey, sigHash, finalSig,
1041+
)
1042+
if err != nil {
1043+
return nil, fmt.Errorf("verifySchnorrSig failed: %w",
1044+
err)
1045+
}
1046+
1047+
return finalSig, nil
1048+
}
1049+
10111050
// Now we're creating a local MuSig2 session using the receiver key's
10121051
// key locator and the htlc's root hash.
10131052
keyLocator := &sweep.htlcKeys.ClientScriptKeyLocator
@@ -1052,9 +1091,6 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
10521091
"nonces missing")
10531092
}
10541093

1055-
var digest [32]byte
1056-
copy(digest[:], sigHash)
1057-
10581094
// Since our MuSig2 session has all nonces, we can now create
10591095
// the local partial signature by signing the sig hash.
10601096
_, err = b.signerClient.MuSig2Sign(

sweepbatcher/sweep_batcher.go

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/btcsuite/btcd/btcec/v2"
1111
"github.com/btcsuite/btcd/btcutil"
1212
"github.com/btcsuite/btcd/chaincfg"
13+
"github.com/btcsuite/btcd/chaincfg/chainhash"
1314
"github.com/btcsuite/btcd/wire"
1415
"github.com/lightninglabs/lndclient"
1516
"github.com/lightninglabs/loop/loopdb"
@@ -140,6 +141,12 @@ type MuSig2SignSweep func(ctx context.Context,
140141
prevoutMap map[wire.OutPoint]*wire.TxOut) (
141142
[]byte, []byte, error)
142143

144+
// SignMuSig2 is a function that can be used to sign a sweep transaction in a
145+
// custom way.
146+
type SignMuSig2 func(ctx context.Context, muSig2Version input.MuSig2Version,
147+
swapHash lntypes.Hash, rootHash chainhash.Hash, sigHash [32]byte,
148+
) ([]byte, error)
149+
143150
// VerifySchnorrSig is a function that can be used to verify a schnorr
144151
// signature.
145152
type VerifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
@@ -245,6 +252,12 @@ type Batcher struct {
245252
// the caller has to update it in the source of SweepInfo (interface
246253
// SweepFetcher) and re-add the sweep by calling AddSweep.
247254
noBumping bool
255+
256+
// customMuSig2Signer is a custom signer. If it is set, it is used to
257+
// create musig2 signatures instead of musig2SignSweep and signerClient.
258+
// Note that musig2SignSweep must be nil in this case, however signer
259+
// client must still be provided, as it is used for non-coop spendings.
260+
customMuSig2Signer SignMuSig2
248261
}
249262

250263
// BatcherConfig holds batcher configuration.
@@ -254,6 +267,12 @@ type BatcherConfig struct {
254267
// the caller has to update it in the source of SweepInfo (interface
255268
// SweepFetcher) and re-add the sweep by calling AddSweep.
256269
noBumping bool
270+
271+
// customMuSig2Signer is a custom signer. If it is set, it is used to
272+
// create musig2 signatures instead of musig2SignSweep and signerClient.
273+
// Note that musig2SignSweep must be nil in this case, however signer
274+
// client must still be provided, as it is used for non-coop spendings.
275+
customMuSig2Signer SignMuSig2
257276
}
258277

259278
// BatcherOption configures batcher behaviour.
@@ -269,6 +288,17 @@ func WithNoBumping() BatcherOption {
269288
}
270289
}
271290

291+
// WithCustomSignMuSig2 instructs sweepbatcher to use a custom function to
292+
// produce MuSig2 signatures. If it is set, it is used to create
293+
// musig2 signatures instead of musig2SignSweep and signerClient. Note
294+
// that musig2SignSweep must be nil in this case, however signerClient
295+
// must still be provided, as it is used for non-coop spendings.
296+
func WithCustomSignMuSig2(customMuSig2Signer SignMuSig2) BatcherOption {
297+
return func(cfg *BatcherConfig) {
298+
cfg.customMuSig2Signer = customMuSig2Signer
299+
}
300+
}
301+
272302
// NewBatcher creates a new Batcher instance.
273303
func NewBatcher(wallet lndclient.WalletKitClient,
274304
chainNotifier lndclient.ChainNotifierClient,
@@ -282,21 +312,27 @@ func NewBatcher(wallet lndclient.WalletKitClient,
282312
opt(&cfg)
283313
}
284314

315+
if cfg.customMuSig2Signer != nil && musig2ServerSigner != nil {
316+
panic("customMuSig2Signer must not be used with " +
317+
"musig2ServerSigner")
318+
}
319+
285320
return &Batcher{
286-
batches: make(map[int32]*batch),
287-
sweepReqs: make(chan SweepRequest),
288-
errChan: make(chan error, 1),
289-
quit: make(chan struct{}),
290-
initDone: make(chan struct{}),
291-
wallet: wallet,
292-
chainNotifier: chainNotifier,
293-
signerClient: signerClient,
294-
musig2ServerSign: musig2ServerSigner,
295-
VerifySchnorrSig: verifySchnorrSig,
296-
chainParams: chainparams,
297-
store: store,
298-
sweepStore: sweepStore,
299-
noBumping: cfg.noBumping,
321+
batches: make(map[int32]*batch),
322+
sweepReqs: make(chan SweepRequest),
323+
errChan: make(chan error, 1),
324+
quit: make(chan struct{}),
325+
initDone: make(chan struct{}),
326+
wallet: wallet,
327+
chainNotifier: chainNotifier,
328+
signerClient: signerClient,
329+
musig2ServerSign: musig2ServerSigner,
330+
VerifySchnorrSig: verifySchnorrSig,
331+
chainParams: chainparams,
332+
store: store,
333+
sweepStore: sweepStore,
334+
noBumping: cfg.noBumping,
335+
customMuSig2Signer: cfg.customMuSig2Signer,
300336
}
301337
}
302338

@@ -456,6 +492,7 @@ func (b *Batcher) spinUpBatch(ctx context.Context) (*batch, error) {
456492
cfg := batchConfig{
457493
maxTimeoutDistance: defaultMaxTimeoutDistance,
458494
noBumping: b.noBumping,
495+
customMuSig2Signer: b.customMuSig2Signer,
459496
}
460497

461498
switch b.chainParams {
@@ -574,6 +611,7 @@ func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error {
574611
cfg := batchConfig{
575612
maxTimeoutDistance: batch.cfg.maxTimeoutDistance,
576613
noBumping: b.noBumping,
614+
customMuSig2Signer: b.customMuSig2Signer,
577615
}
578616

579617
newBatch, err := NewBatchFromDB(cfg, batchKit)
@@ -637,6 +675,7 @@ func (b *Batcher) FetchUnconfirmedBatches(ctx context.Context) ([]*batch,
637675
bchCfg := batchConfig{
638676
maxTimeoutDistance: bch.MaxTimeoutDistance,
639677
noBumping: b.noBumping,
678+
customMuSig2Signer: b.customMuSig2Signer,
640679
}
641680
batch.cfg = &bchCfg
642681

sweepbatcher/sweep_batcher_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/lightninglabs/loop/test"
1717
"github.com/lightninglabs/loop/utils"
1818
"github.com/lightningnetwork/lnd/chainntnfs"
19+
"github.com/lightningnetwork/lnd/input"
1920
"github.com/lightningnetwork/lnd/keychain"
2021
"github.com/lightningnetwork/lnd/lntypes"
2122
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -68,6 +69,18 @@ func testMuSig2SignSweep(ctx context.Context,
6869
return nil, nil, nil
6970
}
7071

72+
var customSignature = func() []byte {
73+
sig := [64]byte{10, 20, 30}
74+
return sig[:]
75+
}()
76+
77+
func testSignMuSig2func(ctx context.Context, muSig2Version input.MuSig2Version,
78+
swapHash lntypes.Hash, rootHash chainhash.Hash,
79+
sigHash [32]byte) ([]byte, error) {
80+
81+
return customSignature, nil
82+
}
83+
7184
var dummyNotifier = SpendNotifier{
7285
SpendChan: make(chan *SpendDetail, ntfnBufferSize),
7386
SpendErrChan: make(chan error, ntfnBufferSize),
@@ -1985,6 +1998,89 @@ func testSweepBatcherCloseDuringAdding(t *testing.T, store testStore,
19851998
<-registrationChan
19861999
}
19872000

2001+
// testCustomSignMuSig2 tests the operation with custom musig2 signer.
2002+
func testCustomSignMuSig2(t *testing.T, store testStore,
2003+
batcherStore testBatcherStore) {
2004+
2005+
defer test.Guard(t)()
2006+
2007+
lnd := test.NewMockLnd()
2008+
ctx, cancel := context.WithCancel(context.Background())
2009+
2010+
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
2011+
require.NoError(t, err)
2012+
2013+
// Use custom MuSig2 signer function.
2014+
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
2015+
nil, testVerifySchnorrSig, lnd.ChainParams, batcherStore,
2016+
sweepStore, WithCustomSignMuSig2(testSignMuSig2func))
2017+
2018+
var wg sync.WaitGroup
2019+
wg.Add(1)
2020+
2021+
var runErr error
2022+
go func() {
2023+
defer wg.Done()
2024+
runErr = batcher.Run(ctx)
2025+
}()
2026+
2027+
// Wait for the batcher to be initialized.
2028+
<-batcher.initDone
2029+
2030+
// Create a sweep request.
2031+
sweepReq := SweepRequest{
2032+
SwapHash: lntypes.Hash{1, 1, 1},
2033+
Value: 111,
2034+
Outpoint: wire.OutPoint{
2035+
Hash: chainhash.Hash{1, 1},
2036+
Index: 1,
2037+
},
2038+
Notifier: &dummyNotifier,
2039+
}
2040+
2041+
swap := &loopdb.LoopOutContract{
2042+
SwapContract: loopdb.SwapContract{
2043+
CltvExpiry: 111,
2044+
AmountRequested: 111,
2045+
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
2046+
HtlcKeys: loopdb.HtlcKeys{
2047+
SenderScriptKey: senderKey,
2048+
ReceiverScriptKey: receiverKey,
2049+
SenderInternalPubKey: senderKey,
2050+
ReceiverInternalPubKey: receiverKey,
2051+
},
2052+
},
2053+
2054+
DestAddr: destAddr,
2055+
SwapInvoice: swapInvoice,
2056+
SweepConfTarget: 111,
2057+
}
2058+
2059+
err = store.CreateLoopOut(ctx, sweepReq.SwapHash, swap)
2060+
require.NoError(t, err)
2061+
store.AssertLoopOutStored()
2062+
2063+
// Deliver sweep request to batcher.
2064+
require.NoError(t, batcher.AddSweep(&sweepReq))
2065+
2066+
// Since a batch was created we check that it registered for its primary
2067+
// sweep's spend.
2068+
<-lnd.RegisterSpendChannel
2069+
2070+
// Wait for tx to be published.
2071+
tx := <-lnd.TxPublishChannel
2072+
2073+
// Check the signature.
2074+
gotSig := tx.TxIn[0].Witness[0]
2075+
require.Equal(t, customSignature, gotSig, "signatures don't match")
2076+
2077+
// Now make the batcher quit by canceling the context.
2078+
cancel()
2079+
wg.Wait()
2080+
2081+
checkBatcherError(t, runErr)
2082+
}
2083+
19882084
// TestSweepBatcherBatchCreation tests that sweep requests enter the expected
19892085
// batch based on their timeout distance.
19902086
func TestSweepBatcherBatchCreation(t *testing.T) {
@@ -2070,6 +2166,11 @@ func TestSweepBatcherCloseDuringAdding(t *testing.T) {
20702166
runTests(t, testSweepBatcherCloseDuringAdding)
20712167
}
20722168

2169+
// TestCustomSignMuSig2 tests the operation with custom musig2 signer.
2170+
func TestCustomSignMuSig2(t *testing.T) {
2171+
runTests(t, testCustomSignMuSig2)
2172+
}
2173+
20732174
// testBatcherStore is BatcherStore used in tests.
20742175
type testBatcherStore interface {
20752176
BatcherStore

0 commit comments

Comments
 (0)