Skip to content

Commit 099e980

Browse files
sputn1ckhieblmi
authored andcommitted
staticaddr/loopin: add listening for sweep request
This commit adds the listening for sweep requests from the server. On a sweep request the loopin manager will fetch the loop in from the db, do sanity and safety checks and then sign the psbt for the input and send it back to the server.
1 parent 409b2e9 commit 099e980

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

staticaddr/loopin/interface.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/lightninglabs/loop/staticaddr/address"
1010
"github.com/lightninglabs/loop/staticaddr/deposit"
1111
"github.com/lightninglabs/loop/staticaddr/script"
12+
"github.com/lightninglabs/loop/swapserverrpc"
1213
"github.com/lightningnetwork/lnd/lntypes"
1314
"github.com/lightningnetwork/lnd/routing/route"
1415
"github.com/lightningnetwork/lnd/zpay32"
@@ -77,3 +78,11 @@ type QuoteGetter interface {
7778
routeHints [][]zpay32.HopHint,
7879
initiator string, numDeposits uint32) (*loop.LoopInQuote, error)
7980
}
81+
82+
type NotificationManager interface {
83+
// SubscribeStaticLoopInSweepRequests subscribes to the static loop in
84+
// sweep requests. These are sent by the server to the client to request
85+
// a sweep of a static loop in that has been finished.
86+
SubscribeStaticLoopInSweepRequests(ctx context.Context,
87+
) <-chan *swapserverrpc.ServerStaticLoopInSweepNotification
88+
}

staticaddr/loopin/manager.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
package loopin
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"sync/atomic"
78
"time"
89

10+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
11+
"github.com/btcsuite/btcd/btcutil/psbt"
912
"github.com/btcsuite/btcd/chaincfg"
13+
"github.com/btcsuite/btcd/chaincfg/chainhash"
14+
"github.com/btcsuite/btcd/txscript"
15+
"github.com/btcsuite/btcd/wire"
1016
"github.com/lightninglabs/lndclient"
1117
"github.com/lightninglabs/loop"
1218
"github.com/lightninglabs/loop/fsm"
1319
"github.com/lightninglabs/loop/labels"
1420
"github.com/lightninglabs/loop/staticaddr/deposit"
21+
"github.com/lightninglabs/loop/swapserverrpc"
1522
looprpc "github.com/lightninglabs/loop/swapserverrpc"
1623
"github.com/lightningnetwork/lnd/lntypes"
1724
"github.com/lightningnetwork/lnd/routing/route"
1825
)
1926

27+
const (
28+
// SwapNotFinishedMsg is the message that is sent to the server if a
29+
// swap is not considered finished yet.
30+
SwapNotFinishedMsg = "swap not finished yet"
31+
)
32+
2033
// Config contains the services required for the loop-in manager.
2134
type Config struct {
2235
// Server is the client that is used to communicate with the static
@@ -63,6 +76,10 @@ type Config struct {
6376
// loop-in related records.
6477
Store StaticAddressLoopInStore
6578

79+
// NotificationManager is the manager that handles the notification
80+
// subscriptions.
81+
NotificationManager NotificationManager
82+
6683
// ValidateLoopInContract validates the contract parameters against our
6784
// request.
6885
ValidateLoopInContract ValidateLoopInContract
@@ -150,6 +167,12 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
150167
return err
151168
}
152169

170+
// Register for notifications of loop-in sweep requests.
171+
sweepReqs := m.cfg.NotificationManager.
172+
SubscribeStaticLoopInSweepRequests(
173+
ctx,
174+
)
175+
153176
// Communicate to the caller that the address manager has completed its
154177
// initialization.
155178
close(m.initChan)
@@ -189,12 +212,204 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
189212
return ctx.Err()
190213
}
191214

215+
case sweepReq := <-sweepReqs:
216+
err = m.handleLoopInSweepReq(ctx, sweepReq)
217+
if err != nil {
218+
log.Errorf("Error handling loop-in sweep "+
219+
"request: %v", err)
220+
}
221+
192222
case <-ctx.Done():
193223
return ctx.Err()
194224
}
195225
}
196226
}
197227

228+
// notifyNotFinished notifies the server that a swap is not finished by
229+
// sending the defined error message.
230+
func (m *Manager) notifyNotFinished(ctx context.Context, swapHash lntypes.Hash,
231+
txId chainhash.Hash) error {
232+
233+
_, err := m.cfg.Server.PushStaticAddressSweeplessSigs(
234+
ctx, &looprpc.PushStaticAddressSweeplessSigsRequest{
235+
SwapHash: swapHash[:],
236+
Txid: txId[:],
237+
ErrorMessage: SwapNotFinishedMsg,
238+
})
239+
240+
return err
241+
}
242+
243+
// handleLoopInSweepReq handles a loop-in sweep request from the server.
244+
// It first checks if the requested loop-in is finished as expected and if
245+
// yes will send signature to the server for the provided psbt.
246+
func (m *Manager) handleLoopInSweepReq(ctx context.Context,
247+
req *swapserverrpc.ServerStaticLoopInSweepNotification) error {
248+
249+
// First we'll check if the loop-ins are known to us and in
250+
// the expected state.
251+
swapHash, err := lntypes.MakeHash(req.SwapHash)
252+
if err != nil {
253+
return err
254+
}
255+
256+
// Fetch the loop-in from the store.
257+
loopIn, err := m.cfg.Store.GetLoopInByHash(ctx, swapHash)
258+
if err != nil {
259+
return err
260+
}
261+
262+
loopIn.AddressParams, err =
263+
m.cfg.AddressManager.GetStaticAddressParameters(ctx)
264+
265+
if err != nil {
266+
return err
267+
}
268+
269+
loopIn.Address, err = m.cfg.AddressManager.GetStaticAddress(ctx)
270+
if err != nil {
271+
return err
272+
}
273+
274+
reader := bytes.NewReader(req.SweepTxPsbt)
275+
sweepPacket, err := psbt.NewFromRawBytes(reader, false)
276+
if err != nil {
277+
return err
278+
}
279+
280+
sweepTx := sweepPacket.UnsignedTx
281+
282+
// If the loop-in is not in the Succeeded state we return an
283+
// error.
284+
if !loopIn.IsInState(Succeeded) {
285+
// We'll notify the server that we don't consider the swap
286+
// finished yet, so it can retry later.
287+
_ = m.notifyNotFinished(ctx, swapHash, sweepTx.TxHash())
288+
return fmt.Errorf("loop-in %v not in Succeeded state",
289+
swapHash)
290+
}
291+
292+
// Perform a sanity check on the number of unsigned tx inputs and
293+
// prevout info.
294+
if len(sweepTx.TxIn) != len(req.PrevoutInfo) {
295+
return fmt.Errorf("expected %v inputs, got %v",
296+
len(req.PrevoutInfo), len(sweepTx.TxIn))
297+
}
298+
299+
// Check if all the deposits requested are part of the loop-in and
300+
// find them in the requested sweep.
301+
depositToIdxMap, err := mapDepositsToIndices(req, loopIn, sweepTx)
302+
if err != nil {
303+
return err
304+
}
305+
306+
prevoutMap := make(map[wire.OutPoint]*wire.TxOut, len(req.PrevoutInfo))
307+
308+
// Set all the prevouts in the prevout map.
309+
for _, prevout := range req.PrevoutInfo {
310+
txid, err := chainhash.NewHash(prevout.TxidBytes)
311+
if err != nil {
312+
return err
313+
}
314+
315+
prevoutMap[wire.OutPoint{
316+
Hash: *txid,
317+
Index: prevout.OutputIndex,
318+
}] = &wire.TxOut{
319+
Value: int64(prevout.Value),
320+
PkScript: prevout.PkScript,
321+
}
322+
}
323+
324+
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(
325+
prevoutMap,
326+
)
327+
328+
sigHashes := txscript.NewTxSigHashes(
329+
sweepPacket.UnsignedTx, prevOutputFetcher,
330+
)
331+
332+
// We'll now sign for every deposit that is part of the loop-in.
333+
responseMap := make(
334+
map[string]*looprpc.ClientSweeplessSigningInfo,
335+
len(req.DepositToNonces),
336+
)
337+
338+
for depositOutpoint, nonce := range req.DepositToNonces {
339+
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
340+
sigHashes, txscript.SigHashDefault,
341+
sweepPacket.UnsignedTx,
342+
depositToIdxMap[depositOutpoint], prevOutputFetcher,
343+
)
344+
if err != nil {
345+
return err
346+
}
347+
348+
var (
349+
serverNonce [musig2.PubNonceSize]byte
350+
sigHash [32]byte
351+
)
352+
353+
copy(serverNonce[:], nonce)
354+
musig2Session, err := loopIn.createMusig2Session(
355+
ctx, m.cfg.Signer,
356+
)
357+
if err != nil {
358+
return err
359+
}
360+
// We'll clean up the session if we don't get to signing.
361+
defer func() {
362+
err = m.cfg.Signer.MuSig2Cleanup(
363+
ctx, musig2Session.SessionID,
364+
)
365+
if err != nil {
366+
log.Errorf("Error cleaning up musig2 session: "+
367+
" %v", err)
368+
}
369+
}()
370+
371+
haveAllNonces, err := m.cfg.Signer.MuSig2RegisterNonces(
372+
ctx, musig2Session.SessionID,
373+
[][musig2.PubNonceSize]byte{serverNonce},
374+
)
375+
if err != nil {
376+
return err
377+
}
378+
379+
if !haveAllNonces {
380+
return fmt.Errorf("expected all nonces to be " +
381+
"registered")
382+
}
383+
384+
copy(sigHash[:], taprootSigHash)
385+
386+
// Since our MuSig2 session has all nonces, we can now create
387+
// the local partial signature by signing the sig hash.
388+
sig, err := m.cfg.Signer.MuSig2Sign(
389+
ctx, musig2Session.SessionID, sigHash, false,
390+
)
391+
if err != nil {
392+
return err
393+
}
394+
395+
responseMap[depositOutpoint] = &looprpc.ClientSweeplessSigningInfo{ //nolint:lll
396+
Nonce: musig2Session.PublicNonce[:],
397+
Sig: sig,
398+
}
399+
}
400+
401+
txHash := sweepTx.TxHash()
402+
403+
_, err = m.cfg.Server.PushStaticAddressSweeplessSigs(
404+
ctx, &looprpc.PushStaticAddressSweeplessSigsRequest{
405+
SwapHash: loopIn.SwapHash[:],
406+
Txid: txHash[:],
407+
SigningInfo: responseMap,
408+
},
409+
)
410+
return err
411+
}
412+
198413
// recover stars a loop-in state machine for each non-final loop-in to pick up
199414
// work where it was left off before the restart.
200415
func (m *Manager) recoverLoopIns(ctx context.Context) error {
@@ -491,3 +706,49 @@ func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn,
491706

492707
return swaps, nil
493708
}
709+
710+
// mapDepositsToIndices maps the deposit outpoints to their respective indices
711+
// in the sweep transaction.
712+
func mapDepositsToIndices(req *swapserverrpc.ServerStaticLoopInSweepNotification, //nolint:lll
713+
loopIn *StaticAddressLoopIn, sweepTx *wire.MsgTx) (map[string]int,
714+
error) {
715+
716+
depositToIdxMap := make(map[string]int)
717+
for reqOutpoint := range req.DepositToNonces {
718+
hasDeposit := false
719+
for _, depositOutpoint := range loopIn.DepositOutpoints {
720+
if depositOutpoint == reqOutpoint {
721+
hasDeposit = true
722+
break
723+
}
724+
}
725+
if !hasDeposit {
726+
return nil, fmt.Errorf("deposit outpoint not part of " +
727+
"loop-in")
728+
}
729+
730+
foundDepositInTx := false
731+
732+
for i, txIn := range sweepTx.TxIn {
733+
if txIn.PreviousOutPoint.String() == reqOutpoint {
734+
// Check that the deposit does not exist in the
735+
// map yet.
736+
if _, ok := depositToIdxMap[reqOutpoint]; ok {
737+
return nil, fmt.Errorf("deposit "+
738+
"outpoint %v already part of "+
739+
"sweep tx", reqOutpoint)
740+
}
741+
742+
depositToIdxMap[reqOutpoint] = i
743+
foundDepositInTx = true
744+
break
745+
}
746+
}
747+
748+
if !foundDepositInTx {
749+
return nil, fmt.Errorf("deposit outpoint %v not part "+
750+
"of sweep tx", reqOutpoint)
751+
}
752+
}
753+
return depositToIdxMap, nil
754+
}

0 commit comments

Comments
 (0)