Skip to content

Commit 3d3e042

Browse files
authored
Merge pull request #160 from bhandras/paginate-listinvoices
challenger: paginage ListInvoices to avoid resource exhaustion
2 parents 25ef16b + 8ec6c28 commit 3d3e042

File tree

2 files changed

+70
-40
lines changed

2 files changed

+70
-40
lines changed

challenger/lnd.go

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"math"
87
"strings"
98
"sync"
109
"time"
@@ -13,6 +12,12 @@ import (
1312
"github.com/lightningnetwork/lnd/lntypes"
1413
)
1514

15+
const (
16+
// invoiceQueryPageSize is the maximum number of invoices that will be
17+
// queried in a single request.
18+
invoiceQueryPageSize = 1000
19+
)
20+
1621
// LndChallenger is a challenger that uses an lnd backend to create new L402
1722
// payment challenges.
1823
type LndChallenger struct {
@@ -74,7 +79,7 @@ func NewLndChallenger(client InvoiceClient,
7479

7580
// Start starts the challenger's main work which is to keep track of all
7681
// invoices and their states. For that the backing lnd node is queried for all
77-
// invoices on startup and the a subscription to all subsequent invoice updates
82+
// invoices on startup and a subscription to all subsequent invoice updates
7883
// is created.
7984
func (l *LndChallenger) Start() error {
8085
// These are the default values for the subscription. In case there are
@@ -84,49 +89,64 @@ func (l *LndChallenger) Start() error {
8489
addIndex := uint64(0)
8590
settleIndex := uint64(0)
8691

87-
// Get a list of all existing invoices on startup and add them to our
88-
// cache. We need to keep track of all invoices, even quite old ones to
89-
// make sure tokens are valid. But to save space we only keep track of
90-
// an invoice's state.
92+
log.Debugf("Starting LND challenger")
93+
// Paginate through all existing invoices on startup and add them to our
94+
// cache. We need to keep track of all invoices to ensure tokens are
95+
// valid.
9196
ctx := l.clientCtx()
92-
invoiceResp, err := l.client.ListInvoices(
93-
ctx, &lnrpc.ListInvoiceRequest{
94-
NumMaxInvoices: math.MaxUint64,
95-
},
96-
)
97-
if err != nil {
98-
return err
99-
}
100-
101-
// Advance our indices to the latest known one so we'll only receive
102-
// updates for new invoices and/or newly settled invoices.
103-
l.invoicesMtx.Lock()
104-
for _, invoice := range invoiceResp.Invoices {
105-
// Some invoices like AMP invoices may not have a payment hash
106-
// populated.
107-
if invoice.RHash == nil {
108-
continue
97+
indexOffset := uint64(0)
98+
for {
99+
log.Debugf("Querying invoices from index %d", indexOffset)
100+
invoiceResp, err := l.client.ListInvoices(
101+
ctx, &lnrpc.ListInvoiceRequest{
102+
IndexOffset: indexOffset,
103+
NumMaxInvoices: invoiceQueryPageSize,
104+
},
105+
)
106+
if err != nil {
107+
return err
109108
}
110109

111-
if invoice.AddIndex > addIndex {
112-
addIndex = invoice.AddIndex
113-
}
114-
if invoice.SettleIndex > settleIndex {
115-
settleIndex = invoice.SettleIndex
116-
}
117-
hash, err := lntypes.MakeHash(invoice.RHash)
118-
if err != nil {
119-
l.invoicesMtx.Unlock()
120-
return fmt.Errorf("error parsing invoice hash: %v", err)
110+
// If there are no more invoices, stop pagination.
111+
if len(invoiceResp.Invoices) == 0 {
112+
break
121113
}
122114

123-
// Don't track the state of canceled or expired invoices.
124-
if invoiceIrrelevant(invoice) {
125-
continue
115+
// Lock the mutex to safely update the invoice states.
116+
l.invoicesMtx.Lock()
117+
for _, invoice := range invoiceResp.Invoices {
118+
// Skip invoices that do not have a payment hash
119+
// populated.
120+
if invoice.RHash == nil {
121+
continue
122+
}
123+
124+
if invoice.AddIndex > addIndex {
125+
addIndex = invoice.AddIndex
126+
}
127+
if invoice.SettleIndex > settleIndex {
128+
settleIndex = invoice.SettleIndex
129+
}
130+
hash, err := lntypes.MakeHash(invoice.RHash)
131+
if err != nil {
132+
l.invoicesMtx.Unlock()
133+
return fmt.Errorf("error parsing invoice "+
134+
"hash: %v", err)
135+
}
136+
137+
// Skip tracking the state of canceled or expired
138+
// invoices.
139+
if invoiceIrrelevant(invoice) {
140+
continue
141+
}
142+
l.invoiceStates[hash] = invoice.State
126143
}
127-
l.invoiceStates[hash] = invoice.State
144+
l.invoicesMtx.Unlock()
145+
146+
// Update the index offset for the next batch.
147+
indexOffset = invoiceResp.LastIndexOffset
128148
}
129-
l.invoicesMtx.Unlock()
149+
log.Debugf("Finished querying invoices")
130150

131151
// We need to be able to cancel any subscription we make.
132152
ctxc, cancel := context.WithCancel(l.clientCtx())

challenger/lnd_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,21 @@ type mockInvoiceClient struct {
4949

5050
// ListInvoices returns a paginated list of all invoices known to lnd.
5151
func (m *mockInvoiceClient) ListInvoices(_ context.Context,
52-
_ *lnrpc.ListInvoiceRequest,
52+
r *lnrpc.ListInvoiceRequest,
5353
_ ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error) {
5454

55+
if r.IndexOffset >= uint64(len(m.invoices)) {
56+
return &lnrpc.ListInvoiceResponse{}, nil
57+
}
58+
59+
endIndex := r.IndexOffset + r.NumMaxInvoices
60+
if endIndex > uint64(len(m.invoices)) {
61+
endIndex = uint64(len(m.invoices))
62+
}
63+
5564
return &lnrpc.ListInvoiceResponse{
56-
Invoices: m.invoices,
65+
Invoices: m.invoices[r.IndexOffset:endIndex],
66+
LastIndexOffset: endIndex,
5767
}, nil
5868
}
5969

0 commit comments

Comments
 (0)