mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loopout: add unit test for the MuSig2 sweep case
This commit is contained in:
parent
82b58e5c0e
commit
5d7b0abdf5
6 changed files with 244 additions and 37 deletions
14
client.go
14
client.go
|
|
@ -9,6 +9,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/aperture/lsat"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
|
|
@ -158,6 +160,18 @@ func NewClient(dbDir string, cfg *ClientConfig) (*Client, func(), error) {
|
|||
totalPaymentTimeout: cfg.TotalPaymentTimeout,
|
||||
maxPaymentRetries: cfg.MaxPaymentRetries,
|
||||
cancelSwap: swapServerClient.CancelLoopOutSwap,
|
||||
verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error {
|
||||
schnorrSig, err := schnorr.ParseSignature(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !schnorrSig.Verify(hash, pubKey) {
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
client := &Client{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
|
|
@ -31,6 +32,8 @@ type executorConfig struct {
|
|||
maxPaymentRetries int
|
||||
|
||||
cancelSwap func(ctx context.Context, details *outCancelDetails) error
|
||||
|
||||
verifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
|
||||
}
|
||||
|
||||
// executor is responsible for executing swaps.
|
||||
|
|
@ -153,6 +156,7 @@ func (s *executor) run(mainCtx context.Context,
|
|||
totalPaymentTimout: s.executorConfig.totalPaymentTimeout,
|
||||
maxPaymentRetries: s.executorConfig.maxPaymentRetries,
|
||||
cancelSwap: s.executorConfig.cancelSwap,
|
||||
verifySchnorrSig: s.executorConfig.verifySchnorrSig,
|
||||
}, height)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.Errorf("Execute error: %v", err)
|
||||
|
|
|
|||
11
loopout.go
11
loopout.go
|
|
@ -10,7 +10,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -96,6 +96,7 @@ type executeConfig struct {
|
|||
totalPaymentTimout time.Duration
|
||||
maxPaymentRetries int
|
||||
cancelSwap func(context.Context, *outCancelDetails) error
|
||||
verifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
|
||||
}
|
||||
|
||||
// loopOutInitResult contains information about a just-initiated loop out swap.
|
||||
|
|
@ -1437,15 +1438,13 @@ func (s *loopOutSwap) createMuSig2SweepTxn(
|
|||
|
||||
// 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.
|
||||
sig, err := schnorr.ParseSignature(finalSig)
|
||||
err = s.executeConfig.verifySchnorrSig(
|
||||
htlc.TaprootKey, sigHash, finalSig,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !sig.Verify(sigHash, htlc.TaprootKey) {
|
||||
return nil, fmt.Errorf("invalid combined signature")
|
||||
}
|
||||
|
||||
// Now that we know the signature is correct, we can fill it in to our
|
||||
// witness.
|
||||
sweepTx.TxIn[0].Witness = wire.TxWitness{
|
||||
|
|
|
|||
238
loopout_test.go
238
loopout_test.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
|
|
@ -87,12 +88,13 @@ func testLoopOutPaymentParameters(t *testing.T) {
|
|||
|
||||
go func() {
|
||||
err := swap.execute(swapCtx, &executeConfig{
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
loopOutMaxParts: maxParts,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
loopOutMaxParts: maxParts,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}, height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
@ -209,11 +211,12 @@ func testLateHtlcPublish(t *testing.T) {
|
|||
errChan := make(chan error)
|
||||
go func() {
|
||||
err := swap.execute(context.Background(), &executeConfig{
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}, height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
@ -320,11 +323,12 @@ func testCustomSweepConfTarget(t *testing.T) {
|
|||
errChan := make(chan error)
|
||||
go func() {
|
||||
err := swap.execute(context.Background(), &executeConfig{
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}, ctx.Lnd.Height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
@ -550,11 +554,12 @@ func testPreimagePush(t *testing.T) {
|
|||
errChan := make(chan error)
|
||||
go func() {
|
||||
err := swap.execute(context.Background(), &executeConfig{
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}, ctx.Lnd.Height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
@ -783,10 +788,11 @@ func testExpiryBeforeReveal(t *testing.T) {
|
|||
errChan := make(chan error)
|
||||
go func() {
|
||||
err := swap.execute(context.Background(), &executeConfig{
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}, ctx.Lnd.Height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
@ -909,11 +915,12 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
errChan := make(chan error)
|
||||
go func() {
|
||||
cfg := &executeConfig{
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
statusChan: statusChan,
|
||||
sweeper: sweeper,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
}
|
||||
|
||||
err := swap.execute(context.Background(), cfg, ctx.Lnd.Height)
|
||||
|
|
@ -1011,3 +1018,174 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
require.Equal(t, state.State, loopdb.StateFailOffchainPayments)
|
||||
require.NoError(t, <-errChan)
|
||||
}
|
||||
|
||||
// TestLoopOutMuSig2Sweep tests the loop out sweep flow when the MuSig2 signing
|
||||
// process is successful.
|
||||
func TestLoopOutMuSig2Sweep(t *testing.T) {
|
||||
defer test.Guard(t)()
|
||||
|
||||
// TODO(bhandras): remove when MuSig2 is default.
|
||||
loopdb.EnableExperimentalProtocol()
|
||||
defer loopdb.ResetCurrentProtocolVersion()
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
ctx := test.NewContext(t, lnd)
|
||||
server := newServerMock(lnd)
|
||||
|
||||
testReq := *testRequest
|
||||
testReq.SweepConfTarget = 10
|
||||
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
|
||||
|
||||
// We set our mock fee estimate for our target sweep confs to be our
|
||||
// max miner fee * 2. With MuSig2 we still expect that the client will
|
||||
// publish the sweep but with the fee clamped to the maximum allowed
|
||||
// miner fee as the preimage is revealed before the sweep txn is
|
||||
// published.
|
||||
ctx.Lnd.SetFeeEstimate(
|
||||
testReq.SweepConfTarget, chainfee.SatPerKWeight(
|
||||
testReq.MaxMinerFee*2,
|
||||
),
|
||||
)
|
||||
|
||||
cfg := newSwapConfig(
|
||||
&lnd.LndServices, newStoreMock(t), server,
|
||||
)
|
||||
|
||||
initResult, err := newLoopOutSwap(
|
||||
context.Background(), cfg, ctx.Lnd.Height, &testReq,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
swap := initResult.swap
|
||||
|
||||
// Set up the required dependencies to execute the swap.
|
||||
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
|
||||
blockEpochChan := make(chan interface{})
|
||||
statusChan := make(chan SwapInfo)
|
||||
expiryChan := make(chan time.Time)
|
||||
timerFactory := func(_ time.Duration) <-chan time.Time {
|
||||
return expiryChan
|
||||
}
|
||||
|
||||
errChan := make(chan error)
|
||||
|
||||
// Mock a successful signature verify to make sure we don't fail
|
||||
// creating the MuSig2 sweep.
|
||||
mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash,
|
||||
sig []byte) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := swap.execute(context.Background(), &executeConfig{
|
||||
statusChan: statusChan,
|
||||
blockEpochChan: blockEpochChan,
|
||||
timerFactory: timerFactory,
|
||||
sweeper: sweeper,
|
||||
cancelSwap: server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigSuccess,
|
||||
}, ctx.Lnd.Height)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// The swap should be found in its initial state.
|
||||
cfg.store.(*storeMock).assertLoopOutStored()
|
||||
state := <-statusChan
|
||||
require.Equal(t, loopdb.StateInitiated, state.State)
|
||||
|
||||
// We'll then pay both the swap and prepay invoice, which should trigger
|
||||
// the server to publish the on-chain HTLC.
|
||||
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
|
||||
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
|
||||
|
||||
signalSwapPaymentResult(nil)
|
||||
signalPrepaymentResult(nil)
|
||||
|
||||
// Notify the confirmation notification for the HTLC.
|
||||
ctx.AssertRegisterConf(false, defaultConfirmations)
|
||||
|
||||
blockEpochChan <- ctx.Lnd.Height + 1
|
||||
|
||||
htlcTx := wire.NewMsgTx(2)
|
||||
htlcTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(swap.AmountRequested),
|
||||
PkScript: swap.htlc.PkScript,
|
||||
})
|
||||
|
||||
ctx.NotifyConf(htlcTx)
|
||||
|
||||
// The client should then register for a spend of the HTLC and attempt
|
||||
// to sweep it using the custom confirmation target.
|
||||
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
|
||||
|
||||
// Assert that we made a query to track our payment, as required for
|
||||
// preimage push tracking.
|
||||
trackPayment := ctx.AssertTrackPayment()
|
||||
|
||||
// Tick the expiry channel, we are still using our client confirmation
|
||||
// target at this stage which has fees higher than our max acceptable
|
||||
// fee. We do not expect a sweep attempt at this point. Since our
|
||||
// preimage is not revealed, we also do not expect a preimage push.
|
||||
expiryChan <- testTime
|
||||
|
||||
// When using taproot htlcs the flow is different as we do reveal the
|
||||
// preimage before sweeping in order for the server to trust us with
|
||||
// our MuSig2 signing attempts.
|
||||
cfg.store.(*storeMock).assertLoopOutState(
|
||||
loopdb.StatePreimageRevealed,
|
||||
)
|
||||
status := <-statusChan
|
||||
require.Equal(
|
||||
t, status.State, loopdb.StatePreimageRevealed,
|
||||
)
|
||||
|
||||
preimage := <-server.preimagePush
|
||||
require.Equal(t, swap.Preimage, preimage)
|
||||
|
||||
// We expect the sweep tx to have been published.
|
||||
ctx.ReceiveTx()
|
||||
|
||||
// Since we don't have a reliable mechanism to non-intrusively avoid
|
||||
// races by setting the fee estimate too soon, let's sleep here a bit
|
||||
// to ensure the first sweep fails.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Now we decrease our fees for the swap's confirmation target to less
|
||||
// than the maximum miner fee.
|
||||
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
|
||||
testReq.MaxMinerFee/2,
|
||||
))
|
||||
|
||||
// Now when we report a new block and tick our expiry fee timer, and
|
||||
// fees are acceptably low so we expect our sweep to be published.
|
||||
blockEpochChan <- ctx.Lnd.Height + 2
|
||||
expiryChan <- testTime
|
||||
|
||||
preimage = <-server.preimagePush
|
||||
require.Equal(t, swap.Preimage, preimage)
|
||||
|
||||
// We expect the sweep tx to have been published.
|
||||
sweepTx := ctx.ReceiveTx()
|
||||
|
||||
// This time, we send a payment succeeded update into our payment stream
|
||||
// to reflect that the server received our preimage push and settled off
|
||||
// chain.
|
||||
trackPayment.Updates <- lndclient.PaymentStatus{
|
||||
State: lnrpc.Payment_SUCCEEDED,
|
||||
}
|
||||
|
||||
// Make sure our sweep tx has a single witness indicating keyspend.
|
||||
require.Len(t, sweepTx.TxIn[0].Witness, 1)
|
||||
|
||||
// Finally, we put this swap out of its misery and notify a successful
|
||||
// spend our our sweepTx and assert that the swap succeeds.
|
||||
ctx.NotifySpend(sweepTx, 0)
|
||||
|
||||
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
|
||||
status = <-statusChan
|
||||
require.Equal(t, status.State, loopdb.StateSuccess)
|
||||
require.NoError(t, <-errChan)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func (s *mockSigner) MuSig2Sign(context.Context, [32]byte, [32]byte,
|
|||
func (s *mockSigner) MuSig2CombineSig(context.Context, [32]byte,
|
||||
[][]byte) (bool, []byte, error) {
|
||||
|
||||
return false, nil, nil
|
||||
return true, nil, nil
|
||||
}
|
||||
|
||||
// MuSig2Cleanup removes a session from memory to free up resources.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package loop
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
|
|
@ -40,6 +42,15 @@ type testContext struct {
|
|||
stop func()
|
||||
}
|
||||
|
||||
// mockVerifySchnorrSigFail is used to simulate failed taproot keyspend
|
||||
// signature verification. If passed to the executeConfig we'll test an
|
||||
// uncooperative server and will fall back to scriptspend sweep.
|
||||
func mockVerifySchnorrSigFail(pubKey *btcec.PublicKey, hash,
|
||||
sig []byte) error {
|
||||
|
||||
return fmt.Errorf("invalid sig")
|
||||
}
|
||||
|
||||
func newSwapClient(config *clientConfig) *Client {
|
||||
sweeper := &sweep.Sweeper{
|
||||
Lnd: config.LndServices,
|
||||
|
|
@ -53,6 +64,7 @@ func newSwapClient(config *clientConfig) *Client {
|
|||
sweeper: sweeper,
|
||||
createExpiryTimer: config.CreateExpiryTimer,
|
||||
cancelSwap: config.Server.CancelLoopOutSwap,
|
||||
verifySchnorrSig: mockVerifySchnorrSigFail,
|
||||
})
|
||||
|
||||
return &Client{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue