mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
multi: pay invoices with allowlisted components
Build outgoing requests from validated invoice fields so encoded server invoices are never forwarded directly to LND.
This commit is contained in:
parent
c977dedc99
commit
3bb9ab1170
9 changed files with 108 additions and 60 deletions
|
|
@ -11,10 +11,10 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil/v2"
|
||||
"github.com/btcsuite/btcd/txscript/v2"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/payment"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
|
|
@ -150,17 +150,19 @@ func (f *FSM) InitInstantOutAction(ctx context.Context,
|
|||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
// Decode the invoice to check if the hash is valid.
|
||||
payReq, err := f.cfg.LndClient.DecodePaymentRequest(
|
||||
ctx, instantOutResponse.SwapInvoice,
|
||||
// Decode and validate the invoice before storing it for payment.
|
||||
paymentRequest, err := payment.RequestFromInvoice(
|
||||
f.cfg.Network, instantOutResponse.SwapInvoice,
|
||||
f.clck.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
if swapHash != payReq.Hash {
|
||||
invoiceHash := *paymentRequest.PaymentHash
|
||||
if swapHash != invoiceHash {
|
||||
return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+
|
||||
"expected %x got %x", preimage.Hash(), payReq.Hash))
|
||||
"expected %x got %x", preimage.Hash(), invoiceHash))
|
||||
}
|
||||
serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey)
|
||||
if err != nil {
|
||||
|
|
@ -212,6 +214,16 @@ func (f *FSM) InitInstantOutAction(ctx context.Context,
|
|||
func (f *FSM) PollPaymentAcceptedAction(ctx context.Context,
|
||||
_ fsm.EventContext) fsm.EventType {
|
||||
|
||||
paymentRequest, err := payment.RequestFromInvoice(
|
||||
f.cfg.Network, f.InstantOut.swapInvoice, f.clck.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
paymentRequest.Timeout = defaultSendpaymentTimeout
|
||||
paymentRequest.MaxParts = defaultMaxParts
|
||||
paymentRequest.MaxFee = getMaxRoutingFee(f.InstantOut.Value)
|
||||
|
||||
// Now that we're doing the swap, we first lock the reservations
|
||||
// so that they can't be used for other swaps.
|
||||
for _, reservation := range f.InstantOut.Reservations {
|
||||
|
|
@ -225,13 +237,7 @@ func (f *FSM) PollPaymentAcceptedAction(ctx context.Context,
|
|||
|
||||
// Now we send the payment to the server.
|
||||
payChan, paymentErrChan, err := f.cfg.RouterClient.SendPayment(
|
||||
ctx,
|
||||
lndclient.SendPaymentRequest{
|
||||
Invoice: f.InstantOut.swapInvoice,
|
||||
Timeout: defaultSendpaymentTimeout,
|
||||
MaxParts: defaultMaxParts,
|
||||
MaxFee: getMaxRoutingFee(f.InstantOut.Value),
|
||||
},
|
||||
ctx, paymentRequest,
|
||||
)
|
||||
if err != nil {
|
||||
f.Errorf("error sending payment: %v", err)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ type Config struct {
|
|||
// Store is used to store the instant out.
|
||||
Store InstantLoopOutStore
|
||||
|
||||
// LndClient is used to decode the swap invoice.
|
||||
// LndClient is used to query lnd.
|
||||
LndClient lndclient.LightningClient
|
||||
|
||||
// RouterClient is used to send the offchain payment to the server.
|
||||
|
|
|
|||
67
loopout.go
67
loopout.go
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/btcsuite/btcd/wire/v2"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/payment"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
|
|
@ -772,15 +773,18 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
|
|||
pluginType RoutingPluginType, reportPluginResult bool, rfqId []byte) (
|
||||
*lndclient.PaymentStatus, error) {
|
||||
|
||||
// Extract hash from payment request. Unfortunately the request
|
||||
// components aren't available directly.
|
||||
chainParams := s.lnd.ChainParams
|
||||
target, routeHints, hash, amt, err := swap.DecodeInvoice(
|
||||
chainParams, invoice,
|
||||
// Decode and validate the invoice before copying its supported fields
|
||||
// into a component-based payment request.
|
||||
req, err := payment.RequestFromInvoice(
|
||||
s.lnd.ChainParams, invoice, s.clock.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := req.Target
|
||||
routeHints := req.RouteHints
|
||||
hash := *req.PaymentHash
|
||||
amt := req.AmountMsat.ToSatoshis()
|
||||
|
||||
maxRetries := 1
|
||||
totalPaymentTimeout := s.executeConfig.totalPaymentTimeout
|
||||
|
|
@ -824,13 +828,10 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
|
|||
paymentTimeout = totalPaymentTimeout
|
||||
}
|
||||
|
||||
req := lndclient.SendPaymentRequest{
|
||||
MaxFee: maxFee,
|
||||
Invoice: invoice,
|
||||
OutgoingChanIds: outgoingChanIds,
|
||||
Timeout: paymentTimeout,
|
||||
MaxParts: s.executeConfig.loopOutMaxParts,
|
||||
}
|
||||
req.MaxFee = maxFee
|
||||
req.OutgoingChanIds = outgoingChanIds
|
||||
req.Timeout = paymentTimeout
|
||||
req.MaxParts = s.executeConfig.loopOutMaxParts
|
||||
|
||||
// If we want an asset swap, we'll need to set the custom first hop
|
||||
// data to the rfq id. This will then allow LND to route the payment
|
||||
|
|
@ -1667,11 +1668,11 @@ trackChanLoop:
|
|||
// resumeLoopOutPayment attempts to resume the loop out payment for the
|
||||
// specified swap.
|
||||
func (m *resumeManager) resumeLoopOutPayment(ctx context.Context,
|
||||
swap *loopdb.LoopOut) error {
|
||||
pendingSwap *loopdb.LoopOut) error {
|
||||
|
||||
swapRes, err := m.swapClient.NewLoopOutSwap(
|
||||
ctx, &swapserverrpc.ServerLoopOutRequest{
|
||||
SwapHash: swap.Hash[:],
|
||||
SwapHash: pendingSwap.Hash[:],
|
||||
UserAgent: ResumeSwapInitiator,
|
||||
},
|
||||
)
|
||||
|
|
@ -1679,33 +1680,35 @@ func (m *resumeManager) resumeLoopOutPayment(ctx context.Context,
|
|||
return err
|
||||
}
|
||||
|
||||
paymentReq := swapRes.SwapInvoice
|
||||
// Verify the payment request before attempting payment.
|
||||
inv, err := m.lnd.Client.DecodePaymentRequest(ctx, paymentReq)
|
||||
// Verify the payment request before attempting payment and copy only
|
||||
// supported invoice fields into the outgoing request.
|
||||
paymentRequest, err := payment.RequestFromInvoice(
|
||||
m.lnd.ChainParams, swapRes.SwapInvoice, m.clock.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode loop out invoice: %v", err)
|
||||
}
|
||||
|
||||
if swap.Hash != inv.Hash {
|
||||
invoiceHash := *paymentRequest.PaymentHash
|
||||
if pendingSwap.Hash != invoiceHash {
|
||||
return fmt.Errorf("invoice payment hash %v does not match "+
|
||||
"swap hash %v", inv.Hash, swap.Hash)
|
||||
"swap hash %v", invoiceHash, pendingSwap.Hash)
|
||||
}
|
||||
|
||||
amtRequested := swap.Contract.AmountRequested
|
||||
amtRequested := pendingSwap.Contract.AmountRequested
|
||||
invoiceAmount := paymentRequest.AmountMsat.ToSatoshis()
|
||||
|
||||
if inv.Value.ToSatoshis() > swap.Contract.MaxSwapFee*2+amtRequested {
|
||||
if invoiceAmount > pendingSwap.Contract.MaxSwapFee*2+amtRequested {
|
||||
return fmt.Errorf("invoice amount %v exceeds max "+
|
||||
"allowed %v", inv.Value.ToSatoshis(),
|
||||
swap.Contract.MaxSwapFee+amtRequested)
|
||||
"allowed %v", invoiceAmount,
|
||||
pendingSwap.Contract.MaxSwapFee+amtRequested)
|
||||
}
|
||||
paymentRequest.Timeout = time.Hour
|
||||
paymentRequest.MaxFee = pendingSwap.Contract.MaxSwapFee
|
||||
|
||||
payChan, errChan, err := m.lnd.Router.SendPayment(
|
||||
ctx,
|
||||
lndclient.SendPaymentRequest{
|
||||
Invoice: paymentReq,
|
||||
Timeout: time.Hour,
|
||||
MaxFee: swap.Contract.MaxSwapFee,
|
||||
})
|
||||
ctx, paymentRequest,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -1716,7 +1719,7 @@ func (m *resumeManager) resumeLoopOutPayment(ctx context.Context,
|
|||
return fmt.Errorf("payment error: %v", payResp.FailureReason)
|
||||
}
|
||||
if payResp.State == lnrpc.Payment_SUCCEEDED {
|
||||
cost := swap.LastUpdate().Cost
|
||||
cost := pendingSwap.LastUpdate().Cost
|
||||
cost.Server = payResp.Value.ToSatoshis() - amtRequested
|
||||
cost.Offchain = payResp.Fee.ToSatoshis()
|
||||
// Payment succeeded.
|
||||
|
|
@ -1724,11 +1727,11 @@ func (m *resumeManager) resumeLoopOutPayment(ctx context.Context,
|
|||
|
||||
// Update state in store.
|
||||
err = m.swapStore.UpdateLoopOut(
|
||||
ctx, swap.Hash, updateTime,
|
||||
ctx, pendingSwap.Hash, updateTime,
|
||||
loopdb.SwapStateData{
|
||||
State: loopdb.StateSuccess,
|
||||
Cost: cost,
|
||||
HtlcTxHash: swap.LastUpdate().HtlcTxHash,
|
||||
HtlcTxHash: pendingSwap.LastUpdate().HtlcTxHash,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func testLoopOutPaymentParameters(t *testing.T) {
|
|||
// Find the swap payment.
|
||||
var swapPayment test.RouterPaymentChannelMessage
|
||||
for _, p := range payments {
|
||||
if p.Invoice == swap.SwapInvoice {
|
||||
if p.PaymentHash != nil && *p.PaymentHash == swap.hash {
|
||||
swapPayment = p
|
||||
}
|
||||
}
|
||||
|
|
@ -894,7 +894,7 @@ func testFailedOffChainCancellation(t *testing.T) {
|
|||
|
||||
// We want to fail our swap payment and succeed the prepayment, so we send
|
||||
// a failure update to the payment that has the larger amount.
|
||||
if pmt1.Amount > pmt2.Amount {
|
||||
if pmt1.AmountMsat > pmt2.AmountMsat {
|
||||
pmt1.TrackPaymentMessage.Updates <- failUpdate
|
||||
pmt2.TrackPaymentMessage.Updates <- successUpdate
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,19 @@ func (s *serverMock) NewLoopOutSwap(_ context.Context, swapHash lntypes.Hash,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Store the remote invoices in the test fixture so component-based
|
||||
// payment assertions can identify them by payment hash.
|
||||
s.lnd.SetInvoice(&lndclient.Invoice{
|
||||
Hash: swapHash,
|
||||
Memo: swapInvoiceDesc,
|
||||
PaymentRequest: swapPayReqString,
|
||||
})
|
||||
s.lnd.SetInvoice(&lndclient.Invoice{
|
||||
Hash: s.prepayHash,
|
||||
Memo: prepayInvoiceDesc,
|
||||
PaymentRequest: prePayReqString,
|
||||
})
|
||||
|
||||
var senderKeyArray [33]byte
|
||||
copy(senderKeyArray[:], senderKey.SerializeCompressed())
|
||||
|
||||
|
|
|
|||
|
|
@ -161,12 +161,15 @@ func (ctx *Context) AssertPaid(
|
|||
expectedMemo)
|
||||
}
|
||||
|
||||
payReq := ctx.DecodeInvoice(swapPayment.SendPaymentRequest.Invoice)
|
||||
paymentHash := swapPayment.SendPaymentRequest.PaymentHash
|
||||
require.NotNil(ctx.T, paymentHash)
|
||||
invoice, ok := ctx.Lnd.LookupInvoice(*paymentHash)
|
||||
require.True(ctx.T, ok, "unknown payment hash: %v", paymentHash)
|
||||
|
||||
_, ok := ctx.PaidInvoices[*payReq.Description]
|
||||
_, ok = ctx.PaidInvoices[invoice.Memo]
|
||||
require.False(
|
||||
ctx.T, ok,
|
||||
"duplicate invoice paid: %v", *payReq.Description,
|
||||
"duplicate invoice paid: %v", invoice.Memo,
|
||||
)
|
||||
|
||||
done := func(result error) {
|
||||
|
|
@ -184,9 +187,9 @@ func (ctx *Context) AssertPaid(
|
|||
}
|
||||
}
|
||||
|
||||
ctx.PaidInvoices[*payReq.Description] = done
|
||||
ctx.PaidInvoices[invoice.Memo] = done
|
||||
|
||||
if *payReq.Description == expectedMemo {
|
||||
if invoice.Memo == expectedMemo {
|
||||
return done
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,6 @@ type mockLightningClient struct {
|
|||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// DecodePaymentRequest returns a non-nil payment request.
|
||||
func (h *mockLightningClient) DecodePaymentRequest(_ context.Context,
|
||||
_ string) (*lndclient.PaymentRequest, error) {
|
||||
|
||||
return &lndclient.PaymentRequest{}, nil
|
||||
}
|
||||
|
||||
func (h *mockLightningClient) WaitForFinished() {
|
||||
h.wg.Wait()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,22 @@ func (s *LndMockServices) SetInvoice(invoice *lndclient.Invoice) {
|
|||
s.Invoices[invoice.Hash] = &invoiceCopy
|
||||
}
|
||||
|
||||
// LookupInvoice returns a copy of the invoice for the payment hash.
|
||||
func (s *LndMockServices) LookupInvoice(hash lntypes.Hash) (
|
||||
*lndclient.Invoice, bool) {
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
invoice, ok := s.Invoices[hash]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
invoiceCopy := *invoice
|
||||
return &invoiceCopy, true
|
||||
}
|
||||
|
||||
// IsDone checks whether all channels have been fully emptied. If not this may
|
||||
// indicate unexpected behaviour of the code under test.
|
||||
func (s *LndMockServices) IsDone() error {
|
||||
|
|
|
|||
|
|
@ -120,8 +120,22 @@ func createClientTestContext(t *testing.T,
|
|||
serverMock := newServerMock(clientLnd)
|
||||
|
||||
store := loopdb.NewStoreMock(t)
|
||||
registerInvoice := func(encoded string) {
|
||||
invoice, err := clientLnd.DecodeInvoice(encoded)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, invoice.PaymentHash)
|
||||
require.NotNil(t, invoice.Description)
|
||||
|
||||
clientLnd.SetInvoice(&lndclient.Invoice{
|
||||
Hash: lntypes.Hash(*invoice.PaymentHash),
|
||||
Memo: *invoice.Description,
|
||||
PaymentRequest: encoded,
|
||||
})
|
||||
}
|
||||
for _, s := range pendingSwaps {
|
||||
store.LoopOutSwaps[s.Hash] = s.Contract
|
||||
registerInvoice(s.Contract.SwapInvoice)
|
||||
registerInvoice(s.Contract.PrepayInvoice)
|
||||
|
||||
updates := []loopdb.SwapStateData{}
|
||||
for _, e := range s.Events {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue