Skip to content

Commit 6e20419

Browse files
committed
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 9c78e0f commit 6e20419

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-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: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
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+
var (
28+
// errSwapNotFinished is returned when a swap is not finished.
29+
errSwapNotFinished = fmt.Errorf("swap not finished")
30+
)
31+
2032
// Config contains the services required for the loop-in manager.
2133
type Config struct {
2234
// Server is the client that is used to communicate with the static
@@ -63,6 +75,10 @@ type Config struct {
6375
// loop-in related records.
6476
Store StaticAddressLoopInStore
6577

78+
// NotificationManager is the manager that handles the notification
79+
// subscriptions.
80+
NotificationManager NotificationManager
81+
6682
// ValidateLoopInContract validates the contract parameters against our
6783
// request.
6884
ValidateLoopInContract ValidateLoopInContract
@@ -150,6 +166,12 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
150166
return err
151167
}
152168

169+
// Register for notifications of loop-in sweep requests.
170+
sweepReqs := m.cfg.NotificationManager.
171+
SubscribeStaticLoopInSweepRequests(
172+
ctx,
173+
)
174+
153175
// Communicate to the caller that the address manager has completed its
154176
// initialization.
155177
close(m.initChan)
@@ -189,12 +211,215 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
189211
return ctx.Err()
190212
}
191213

214+
case sweepReq := <-sweepReqs:
215+
err = m.handleLoopInSweepReq(ctx, sweepReq)
216+
if err != nil {
217+
log.Errorf("Error handling loop-in sweep request: %v",
218+
err)
219+
}
220+
192221
case <-ctx.Done():
193222
return ctx.Err()
194223
}
195224
}
196225
}
197226

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

0 commit comments

Comments
 (0)