Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ffranr committed Apr 30, 2024
1 parent 4e85008 commit 2f1825d
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 194 deletions.
7 changes: 5 additions & 2 deletions invoices/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,13 @@ type InterceptClientRequest struct {
// invoice settlement.
ExitHtlcAmt lnwire.MilliSatoshi

// ExitHtlcExpiry is the expiry of the HTLC which is involved in the
// invoice settlement.
// ExitHtlcExpiry is the expiry time of the HTLC which is involved in
// the invoice settlement.
ExitHtlcExpiry uint32

// CurrentHeight is the current block height.
CurrentHeight uint32

// Invoice is the invoice that is being intercepted.
Invoice Invoice
}
Expand Down
17 changes: 16 additions & 1 deletion invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
)

callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) {
log.Debugf("In callback func. Invoice: %v", inv)

updateDesc, res, err := updateInvoice(ctx, inv)
if err != nil {
return nil, err
Expand All @@ -1016,6 +1018,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
// Assign resolution to outer scope variable.
resolution = res

log.Debugf("SettlementInterceptor: %v", i.cfg.SettlementInterceptor)

// Provide the invoice to the settlement interceptor to allow
// the interceptor's client an opportunity to specify the
// settlement resolution.
Expand All @@ -1025,18 +1029,23 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
ExitHtlcCircuitKey: ctx.circuitKey,
ExitHtlcAmt: ctx.amtPaid,
ExitHtlcExpiry: ctx.expiry,
CurrentHeight: uint32(ctx.currentHeight),
Invoice: *inv,
}
interceptSession =
i.cfg.SettlementInterceptor.Intercept(clientReq)
}

log.Debugf("interceptSession: %v", interceptSession)

// If the interceptor service has provided a response, we'll
// use the interceptor session to wait for the client to respond
// with a settlement resolution.
interceptSession.WhenSome(func(session InterceptSession) {
select {
case resp := <-session.ClientResponseChannel:
log.Debugf("Settlement interceptor response: %v", resp)

// If the interceptor has provided a resolution,
// we'll use that as the final resolution.
resolution = resp.HtlcResolution
Expand All @@ -1057,6 +1066,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
updateSubscribers = updateDesc != nil &&
updateDesc.State != nil

updateSubscribers = true

case <-session.Quit:
// At this point, the interceptor session has
// quit.
Expand Down Expand Up @@ -1104,6 +1115,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(

var invoiceToExpire invoiceExpiry

log.Debugf("Settlement resolution: %T %v", resolution, resolution)

switch res := resolution.(type) {
case *HtlcFailResolution:
// Inspect latest htlc state on the invoice. If it is found,
Expand Down Expand Up @@ -1152,6 +1165,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
// with our peer.
setID := ctx.setID()
settledHtlcSet := invoice.HTLCSet(setID, HtlcStateSettled)
log.Debugf("Settled htlc set size: %d", len(settledHtlcSet))

for key, htlc := range settledHtlcSet {
preimage := res.Preimage
if htlc.AMP != nil && htlc.AMP.Preimage != nil {
Expand Down Expand Up @@ -1236,7 +1251,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
}

// Now that the links have been notified of any state changes to their
// HTLCs, we'll go ahead and notify any clients wiaiting on the invoice
// HTLCs, we'll go ahead and notify any clients waiting on the invoice
// state changes.
if updateSubscribers {
// We'll add a setID onto the notification, but only if this is
Expand Down
165 changes: 60 additions & 105 deletions itest/lnd_invoice_acceptor_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package itest

import (
"context"
"time"

"github.com/btcsuite/btcd/btcutil"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -43,129 +45,82 @@ func testInvoiceAcceptorBasic(ht *lntest.HarnessTest) {
// TODO(ffranr): Handle each test case in turn: send payment, accept
// invoice, and check the final status of the payment.

// For each test case make sure we initiate a payment from Alice to
// Carol routed through Bob. For each payment we also test its final
// status according to the interceptorAction specified in the test
// case.
done := make(chan struct{})
go func() {
// Signal that all the payments have been sent.
defer close(done)
// We make sure here the interceptor has processed all packets before
// we check the payment statuses.
for tcIdx, tc := range testCases {
ht.Logf("Running test case: %d", tcIdx)

// Initiate a payment from Alice to Carol in a separate
// goroutine. We use a separate goroutine to avoid blocking the
// main goroutine where we will make use of the invoice
// acceptor.
sendPaymentDone := make(chan struct{})
go func() {
// Signal that all the payments have been sent.
defer close(sendPaymentDone)

for _, tc := range testCases {
_ = ts.sendPayment(tc)
}
}()
}()

// We make sure here the interceptor has processed all packets before
// we check the payment statuses.
for _, tc := range testCases {
acceptorRequest := ht.ReceiveInvoiceAcceptor(invoiceAcceptor)

require.Equal(ht, tc.invoiceAmountMsat,
require.EqualValues(ht, tc.invoiceAmountMsat,
acceptorRequest.Invoice.ValueMsat)
require.Equal(ht, acceptorRequest.ExitHtlcAmt,
tc.sendAmountMsat)
require.EqualValues(ht, tc.sendAmountMsat,
acceptorRequest.ExitHtlcAmt)

preimage, err := lntypes.MakePreimage(tc.invoice.RPreimage)
require.NoError(ht, err, "failed to parse invoice preimage")

// For all other packets we resolve according to the test case.
err := invoiceAcceptor.Send(
outcome := invoicesrpc.AcceptResolutionResult_ACCEPTED
err = invoiceAcceptor.Send(
&invoicesrpc.InvoiceAcceptorResponse{
CircuitKey: acceptorRequest.ExitHtlcCircuitKey,
AutoRelease: false,
AcceptTime: uint64(time.Now().Unix()),
Outcome: invoicesrpc.AcceptResolutionResult_ACCEPTED,
Preimage: preimage[:],
CircuitKey: acceptorRequest.ExitHtlcCircuitKey,
AutoRelease: false,
AcceptTime: uint64(time.Now().Unix()),
AcceptHeight: uint64(acceptorRequest.CurrentHeight),
Outcome: outcome,
},
)
require.NoError(ht, err, "failed to send request")

time.Sleep(5 * time.Second)

// Check that the invoice has been accepted.
ctx := context.Background()
dbgInfo, err := carol.RPC.LN.GetDebugInfo(
ctx, &lnrpc.GetDebugInfoRequest{},
)
require.NoError(ht, err, "failed to get Carol node debug info")
dbgInfo = dbgInfo

Check failure on line 96 in itest/lnd_invoice_acceptor_test.go

View workflow job for this annotation

GitHub Actions / lint code

assign: self-assignment of dbgInfo to dbgInfo (govet)

logRev := dbgInfo.Log
revlog := func(s []string) {
for i := 0; i < len(s)/2; i++ {
j := len(s) - i - 1
s[i], s[j] = s[j], s[i]
}
}
revlog(logRev)

// Check that the payment send attempt has finished.
ht.Log("Waiting for payment send to complete")
select {
case <-sendPaymentDone:
ht.Log("Payment send attempt completed")
case <-time.After(defaultTimeout):
require.Fail(ht, "timeout waiting for payment send")
}

time.Sleep(7 * time.Second)

// Check that the invoice has been settled.
ht.Log("Checking invoice status")
updatedInvoice := carol.RPC.LookupInvoice(tc.invoice.RHash)
require.Equal(ht, updatedInvoice.State, lnrpc.Invoice_SETTLED)
require.Equal(ht, lnrpc.Invoice_SETTLED, updatedInvoice.State)
}

//// At this point we are left with the held packets, we want to make
//// sure each one of them has a corresponding 'in-flight' payment at
//// Alice's node.
//for _, testCase := range testCases {
// if !testCase.shouldHold {
// continue
// }
//
// var preimage lntypes.Preimage
// copy(preimage[:], testCase.invoice.RPreimage)
//
// payment := ht.AssertPaymentStatus(
// alice, preimage, lnrpc.Payment_IN_FLIGHT,
// )
// expectedAmt := testCase.invoice.ValueMsat
// require.Equal(ht, expectedAmt, payment.ValueMsat,
// "incorrect in flight amount")
//}
//
//// Cancel the context, which will disconnect the above interceptor.
//cancelInterceptor()
//
//// Disconnect interceptor should cause resume held packets. After that
//// we wait for all go routines to finish, including the one that tests
//// the payment final status for the held payment.
//select {
//case <-done:
//case <-time.After(defaultTimeout):
// require.Fail(ht, "timeout waiting for sending payment")
//}
//
//// Verify that we don't get notified about already completed HTLCs
//// We do that by restarting alice, the sender the HTLCs. Under
//// https://github.com/lightningnetwork/lnd/issues/5115
//// this should cause all HTLCs settled or failed by the interceptor to
//// renotify.
//restartAlice := ht.SuspendNode(alice)
//require.NoError(ht, restartAlice(), "failed to restart alice")
//
//// Make sure the channel is active from both Alice and Bob's PoV.
//ht.AssertChannelExists(alice, cpAB)
//ht.AssertChannelExists(bob, cpAB)
//
//// Create a new interceptor as the old one has quit.
//interceptor, cancelInterceptor = bob.RPC.HtlcInterceptor()
//
//done = make(chan struct{})
//go func() {
// defer close(done)
//
// _, err := interceptor.Recv()
// require.Error(ht, err, "expected an error from interceptor")
//
// status, ok := status.FromError(err)
// switch {
// // If it is just the error result of the context cancellation
// // the we exit silently.
// case ok && status.Code() == codes.Canceled:
// fallthrough
//
// // When the test ends, during the node's shutdown it will close
// // the connection.
// case strings.Contains(err.Error(), "closed network connection"):
// fallthrough
//
// case strings.Contains(err.Error(), "EOF"):
// return
// }
//
// // Otherwise we receive an unexpected error.
// require.Failf(ht, "iinterceptor", "unexpected err: %v", err)
//}()
//
//// Cancel the context, which will disconnect the above interceptor.
//cancelInterceptor()
//select {
//case <-done:
//case <-time.After(defaultTimeout):
// require.Fail(ht, "timeout waiting for interceptor error")
//}

cancelInvoiceAcceptor()

// Finally, close channels.
Expand Down
36 changes: 24 additions & 12 deletions lnrpc/invoicesrpc/invoice_acceptor.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package invoicesrpc

import (
"time"

"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
Expand Down Expand Up @@ -87,6 +85,7 @@ func (r *invoiceAcceptor) onIntercept(
ExitHtlcCircuitKey: rpcCircuitKey,
ExitHtlcAmt: uint64(req.ExitHtlcAmt),
ExitHtlcExpiry: req.ExitHtlcExpiry,
CurrentHeight: req.CurrentHeight,
})
}

Expand All @@ -96,6 +95,20 @@ func (r *invoiceAcceptor) resolveFromClient(

log.Tracef("Resolving invoice acceptor response %v", in)

// Parse the invoice preimage from the response.
if len(in.Preimage) != lntypes.HashSize {
return status.Errorf(codes.InvalidArgument,
"Preimage has invalid length: %d", len(in.Preimage))
}
preimage, err := lntypes.MakePreimage(in.Preimage)
if err != nil {
return status.Errorf(codes.InvalidArgument,
"Preimage is invalid: %v", err)
}

// Derive the payment hash from the preimage.
paymentHash := preimage.Hash()

// Parse the circuit key from the response.
if in.CircuitKey == nil {
return status.Errorf(codes.InvalidArgument,
Expand All @@ -106,25 +119,24 @@ func (r *invoiceAcceptor) resolveFromClient(
HtlcID: in.CircuitKey.HtlcId,
}

// Parse the payment hash from the response.
var invoicePaymentHash lntypes.Hash
copy(invoicePaymentHash[:], in.PaymentHash)

// Parse the accept time from the response.
acceptTimeUnix := int64(in.AcceptTime)
acceptTime := time.Unix(acceptTimeUnix, 0).UTC()
//acceptTimeUnix := int64(in.AcceptTime)

Check failure on line 123 in lnrpc/invoicesrpc/invoice_acceptor.go

View workflow job for this annotation

GitHub Actions / lint code

commentFormatting: put a space between `//` and comment text (gocritic)
//acceptTime := time.Unix(acceptTimeUnix, 0).UTC()

// Parse the outcome from the response.
outcome := invoices.AcceptResolutionResult(in.Outcome.Number())
//outcome := invoices.AcceptResolutionResult(in.Outcome.Number())

Check failure on line 127 in lnrpc/invoicesrpc/invoice_acceptor.go

View workflow job for this annotation

GitHub Actions / lint code

commentFormatting: put a space between `//` and comment text (gocritic)

// Construct the HTLC resolution from the response.
htlcResolution := invoices.NewAcceptResolution(
circuitKey, outcome, in.AutoRelease, acceptTime,
//htlcResolution := invoices.NewAcceptResolution(

Check failure on line 130 in lnrpc/invoicesrpc/invoice_acceptor.go

View workflow job for this annotation

GitHub Actions / lint code

commentFormatting: put a space between `//` and comment text (gocritic)
// circuitKey, outcome, in.AutoRelease, acceptTime,
//)
htlcResolution := invoices.NewSettleResolution(
preimage, circuitKey, int32(in.AcceptHeight), 1,
)

// Pass the resolution to the interceptor.
return r.cfg.interceptor.Resolve(
invoicePaymentHash, htlcResolution,
paymentHash, htlcResolution,
fn.None[invoices.InvoiceUpdateDesc](),
)
}

0 comments on commit 2f1825d

Please sign in to comment.