sweepbatcher: add option WithCustomSignMuSig2

It is needed to provide a custom MuSig2 signer.
pull/784/head
Boris Nagaev 2 months ago
parent 38e86a1b0f
commit aa6d748647
No known key found for this signature in database

@ -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(

@ -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

@ -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

Loading…
Cancel
Save