Skip to content

Commit 1f5c270

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 2831e3a commit 1f5c270

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-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"
@@ -74,3 +75,11 @@ type QuoteGetter interface {
7475
routeHints [][]zpay32.HopHint,
7576
initiator string, numDeposits uint32) (*loop.LoopInQuote, error)
7677
}
78+
79+
type NotificationManager interface {
80+
// SubscribeStaticLoopInSweepRequests subscribes to the static loop in
81+
// sweep requests. These are sent by the server to the client to request
82+
// a sweep of a static loop in that has been finished.
83+
SubscribeStaticLoopInSweepRequests(ctx context.Context,
84+
) <-chan *swapserverrpc.ServerStaticLoopInSweepRequest
85+
}

staticaddr/loopin/manager.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
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"
@@ -63,6 +70,10 @@ type Config struct {
6370
// loop-in related records.
6471
Store StaticAddressLoopInStore
6572

73+
// NotificationManager is the manager that handles the notification
74+
// subscriptions.
75+
NotificationManager NotificationManager
76+
6677
// ValidateLoopInContract validates the contract parameters against our
6778
// request.
6879
ValidateLoopInContract ValidateLoopInContract
@@ -150,6 +161,12 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
150161
return err
151162
}
152163

164+
// Register for notifications of loop-in sweep requests.
165+
sweepReqs := m.cfg.NotificationManager.
166+
SubscribeStaticLoopInSweepRequests(
167+
ctx,
168+
)
169+
153170
// Communicate to the caller that the address manager has completed its
154171
// initialization.
155172
close(m.initChan)
@@ -189,12 +206,169 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
189206
return ctx.Err()
190207
}
191208

209+
case sweepReq := <-sweepReqs:
210+
err = m.handleLoopInSweepReq(ctx, sweepReq)
211+
if err != nil {
212+
log.Errorf("Error handling loop-in sweep request: %v",
213+
err)
214+
}
215+
192216
case <-ctx.Done():
193217
return ctx.Err()
194218
}
195219
}
196220
}
197221

222+
// handleLoopInSweepReq handles a loop-in sweep request from the server.
223+
// It first checks if the requested loop-in is finished as expected and if
224+
// yes will send signature to the server for the provided psbt.
225+
func (m *Manager) handleLoopInSweepReq(ctx context.Context,
226+
req *swapserverrpc.ServerStaticLoopInSweepRequest) error {
227+
228+
// First we'll check if the loop-ins are known to us and in
229+
// the expected state.
230+
swapHash, err := lntypes.MakeHash(req.SwapHash)
231+
if err != nil {
232+
return err
233+
}
234+
235+
// Fetch the loop-in from the store.
236+
loopIn, err := m.cfg.Store.FetchLoopInByHash(ctx, swapHash)
237+
if err != nil {
238+
return err
239+
}
240+
241+
// If the loop-in is not in the Succeeded state we return an
242+
// error.
243+
if loopIn.state != Succeeded {
244+
return fmt.Errorf("loop-in %v not in Succeeded state",
245+
swapHash)
246+
}
247+
248+
reader := bytes.NewReader(req.SweepTxPsbt)
249+
sweepPacket, err := psbt.NewFromRawBytes(reader, false)
250+
if err != nil {
251+
return err
252+
}
253+
254+
sweepTx := sweepPacket.UnsignedTx
255+
256+
// Perform a sanity check on the number of unsigned tx inputs and
257+
// prevout info.
258+
if len(sweepTx.TxIn) != len(req.PrevoutInfo) {
259+
return fmt.Errorf("expected %v inputs, got %v",
260+
len(req.PrevoutInfo), len(sweepTx.TxIn))
261+
}
262+
263+
prevoutMap := make(map[wire.OutPoint]*wire.TxOut)
264+
var depositOutpoint *wire.OutPoint
265+
266+
for i := range req.PrevoutInfo {
267+
prevout := req.PrevoutInfo[i]
268+
269+
txid, err := chainhash.NewHash(prevout.TxidBytes)
270+
if err != nil {
271+
return err
272+
}
273+
274+
if i == int(req.OutputIndex) {
275+
depositOutpoint = &wire.OutPoint{
276+
Hash: *txid,
277+
Index: prevout.OutputIndex,
278+
}
279+
}
280+
281+
prevoutMap[wire.OutPoint{
282+
Hash: *txid,
283+
Index: prevout.OutputIndex,
284+
}] = &wire.TxOut{
285+
Value: int64(req.PrevoutInfo[i].Value),
286+
PkScript: req.PrevoutInfo[i].PkScript,
287+
}
288+
}
289+
290+
// Check if the deposit outpoint is part of the loop-in.
291+
if depositOutpoint == nil {
292+
return fmt.Errorf("deposit outpoint not part of loop-in")
293+
}
294+
295+
foundDeposit := false
296+
for _, loopInDeposit := range loopIn.DepositOutpoints {
297+
if loopInDeposit == fmt.Sprintf("%v:%v",
298+
depositOutpoint.Hash, depositOutpoint.Index) {
299+
foundDeposit = true
300+
}
301+
}
302+
303+
if !foundDeposit {
304+
return fmt.Errorf("deposit outpoint not part of loop-in")
305+
}
306+
307+
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(
308+
prevoutMap,
309+
)
310+
311+
sigHashes := txscript.NewTxSigHashes(
312+
sweepPacket.UnsignedTx, prevOutputFetcher,
313+
)
314+
315+
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
316+
sigHashes, txscript.SigHashDefault, sweepPacket.UnsignedTx,
317+
int(req.OutputIndex), prevOutputFetcher,
318+
)
319+
if err != nil {
320+
return err
321+
}
322+
323+
var (
324+
serverNonce [musig2.PubNonceSize]byte
325+
sigHash [32]byte
326+
)
327+
328+
copy(serverNonce[:], req.Nonce)
329+
musig2Session, err := loopIn.createMusig2Session(ctx, m.cfg.Signer)
330+
if err != nil {
331+
return err
332+
}
333+
334+
haveAllNonces, err := m.cfg.Signer.MuSig2RegisterNonces(
335+
ctx, musig2Session.SessionID,
336+
[][musig2.PubNonceSize]byte{serverNonce},
337+
)
338+
if err != nil {
339+
return err
340+
}
341+
342+
if !haveAllNonces {
343+
return fmt.Errorf("expected all nonces to be registered")
344+
}
345+
346+
copy(sigHash[:], taprootSigHash)
347+
348+
// Since our MuSig2 session has all nonces, we can now create
349+
// the local partial signature by signing the sig hash.
350+
sig, err := m.cfg.Signer.MuSig2Sign(
351+
ctx, musig2Session.SessionID, sigHash, false,
352+
)
353+
if err != nil {
354+
return err
355+
}
356+
357+
txHash := sweepTx.TxHash()
358+
359+
// We'll now push the signature to the server.
360+
_, err = m.cfg.Server.PushStaticAddressSweeplessSigs(
361+
ctx, &looprpc.PushStaticAddressSweeplessSigsRequest{
362+
TxHash: txHash[:],
363+
SwapHash: loopIn.SwapHash[:],
364+
OutputIndex: req.OutputIndex,
365+
Nonce: musig2Session.PublicNonce[:],
366+
Sig: sig,
367+
},
368+
)
369+
return err
370+
}
371+
198372
// recover stars a loop-in state machine for each non-final loop-in to pick up
199373
// work where it was left off before the restart.
200374
func (m *Manager) recoverLoopIns(ctx context.Context) error {

0 commit comments

Comments
 (0)