forked from lightninglabs/aperture
-
Notifications
You must be signed in to change notification settings - Fork 0
/
challenger.go
386 lines (326 loc) · 10.7 KB
/
challenger.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
package aperture
import (
"context"
"fmt"
"io"
"math"
"strings"
"sync"
"time"
"github.com/lightninglabs/aperture/auth"
"github.com/lightninglabs/aperture/mint"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc"
)
// InvoiceRequestGenerator is a function type that returns a new request for the
// lnrpc.AddInvoice call.
type InvoiceRequestGenerator func(price int64) (*lnrpc.Invoice, error)
// InvoiceClient is an interface that only implements part of a full lnd client,
// namely the part around the invoices we need for the challenger to work.
type InvoiceClient interface {
// ListInvoices returns a paginated list of all invoices known to lnd.
ListInvoices(ctx context.Context, in *lnrpc.ListInvoiceRequest,
opts ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error)
// SubscribeInvoices subscribes to updates on invoices.
SubscribeInvoices(ctx context.Context, in *lnrpc.InvoiceSubscription,
opts ...grpc.CallOption) (
lnrpc.Lightning_SubscribeInvoicesClient, error)
// AddInvoice adds a new invoice to lnd.
AddInvoice(ctx context.Context, in *lnrpc.Invoice,
opts ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error)
}
// LndChallenger is a challenger that uses an lnd backend to create new LSAT
// payment challenges.
type LndChallenger struct {
client InvoiceClient
genInvoiceReq InvoiceRequestGenerator
invoiceStates map[lntypes.Hash]lnrpc.Invoice_InvoiceState
invoicesMtx *sync.Mutex
invoicesCancel func()
invoicesCond *sync.Cond
errChan chan<- error
quit chan struct{}
wg sync.WaitGroup
}
// A compile time flag to ensure the LndChallenger satisfies the
// mint.Challenger and auth.InvoiceChecker interface.
var _ mint.Challenger = (*LndChallenger)(nil)
var _ auth.InvoiceChecker = (*LndChallenger)(nil)
const (
// invoiceMacaroonName is the name of the invoice macaroon belonging
// to the target lnd node.
invoiceMacaroonName = "invoice.macaroon"
)
// NewLndChallenger creates a new challenger that uses the given connection
// details to connect to an lnd backend to create payment challenges.
func NewLndChallenger(cfg *AuthConfig, genInvoiceReq InvoiceRequestGenerator,
errChan chan<- error) (*LndChallenger, error) {
if genInvoiceReq == nil {
return nil, fmt.Errorf("genInvoiceReq cannot be nil")
}
client, err := lndclient.NewBasicClient(
cfg.LndHost, cfg.TLSPath, cfg.MacDir, cfg.Network,
lndclient.MacFilename(invoiceMacaroonName),
)
if err != nil {
return nil, err
}
invoicesMtx := &sync.Mutex{}
return &LndChallenger{
client: client,
genInvoiceReq: genInvoiceReq,
invoiceStates: make(map[lntypes.Hash]lnrpc.Invoice_InvoiceState),
invoicesMtx: invoicesMtx,
invoicesCond: sync.NewCond(invoicesMtx),
quit: make(chan struct{}),
errChan: errChan,
}, nil
}
// Start starts the challenger's main work which is to keep track of all
// invoices and their states. For that the backing lnd node is queried for all
// invoices on startup and the a subscription to all subsequent invoice updates
// is created.
func (l *LndChallenger) Start() error {
// These are the default values for the subscription. In case there are
// no invoices yet, this will instruct lnd to just send us all updates.
// If there are existing invoices, these indices will be updated to
// reflect the latest known invoices.
addIndex := uint64(0)
settleIndex := uint64(0)
// Get a list of all existing invoices on startup and add them to our
// cache. We need to keep track of all invoices, even quite old ones to
// make sure tokens are valid. But to save space we only keep track of
// an invoice's state.
invoiceResp, err := l.client.ListInvoices(
context.Background(), &lnrpc.ListInvoiceRequest{
NumMaxInvoices: math.MaxUint64,
},
)
if err != nil {
return err
}
// Advance our indices to the latest known one so we'll only receive
// updates for new invoices and/or newly settled invoices.
l.invoicesMtx.Lock()
for _, invoice := range invoiceResp.Invoices {
if invoice.AddIndex > addIndex {
addIndex = invoice.AddIndex
}
if invoice.SettleIndex > settleIndex {
settleIndex = invoice.SettleIndex
}
hash, err := lntypes.MakeHash(invoice.RHash)
if err != nil {
l.invoicesMtx.Unlock()
return fmt.Errorf("error parsing invoice hash: %v", err)
}
// Don't track the state of canceled or expired invoices.
if invoiceIrrelevant(invoice) {
continue
}
l.invoiceStates[hash] = invoice.State
}
l.invoicesMtx.Unlock()
// We need to be able to cancel any subscription we make.
ctxc, cancel := context.WithCancel(context.Background())
l.invoicesCancel = cancel
subscriptionResp, err := l.client.SubscribeInvoices(
ctxc, &lnrpc.InvoiceSubscription{
AddIndex: addIndex,
SettleIndex: settleIndex,
},
)
if err != nil {
cancel()
return err
}
l.wg.Add(1)
go func() {
defer l.wg.Done()
defer cancel()
l.readInvoiceStream(subscriptionResp)
}()
return nil
}
// readInvoiceStream reads the invoice update messages sent on the stream until
// the stream is aborted or the challenger is shutting down.
func (l *LndChallenger) readInvoiceStream(
stream lnrpc.Lightning_SubscribeInvoicesClient) {
for {
// In case we receive the shutdown signal right after receiving
// an update, we can exit early.
select {
case <-l.quit:
return
default:
}
// Wait for an update to arrive. This will block until either a
// message receives, an error occurs or the underlying context
// is canceled (which will also result in an error).
invoice, err := stream.Recv()
switch {
case err == io.EOF:
// The connection is shutting down, we can't continue
// to function properly. Signal the error to the main
// goroutine to force a shutdown/restart.
select {
case l.errChan <- err:
case <-l.quit:
default:
}
return
case err != nil && strings.Contains(
err.Error(), context.Canceled.Error(),
):
// The context has been canceled, we are shutting down.
// So no need to forward the error to the main
// goroutine.
return
case err != nil:
log.Errorf("Received error from invoice subscription: "+
"%v", err)
// The connection is faulty, we can't continue to
// function properly. Signal the error to the main
// goroutine to force a shutdown/restart.
select {
case l.errChan <- err:
case <-l.quit:
default:
}
return
default:
}
hash, err := lntypes.MakeHash(invoice.RHash)
if err != nil {
log.Errorf("Error parsing invoice hash: %v", err)
return
}
l.invoicesMtx.Lock()
if invoiceIrrelevant(invoice) {
// Don't keep the state of canceled or expired invoices.
delete(l.invoiceStates, hash)
} else {
l.invoiceStates[hash] = invoice.State
}
// Before releasing the lock, notify our conditions that listen
// for updates on the invoice state.
l.invoicesCond.Broadcast()
l.invoicesMtx.Unlock()
}
}
// Stop shuts down the challenger.
func (l *LndChallenger) Stop() {
l.invoicesCancel()
close(l.quit)
l.wg.Wait()
}
// NewChallenge creates a new LSAT payment challenge, returning a payment
// request (invoice) and the corresponding payment hash.
//
// NOTE: This is part of the mint.Challenger interface.
func (l *LndChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) {
// Obtain a new invoice from lnd first. We need to know the payment hash
// so we can add it as a caveat to the macaroon.
invoice, err := l.genInvoiceReq(price)
if err != nil {
log.Errorf("Error generating invoice request: %v", err)
return "", lntypes.ZeroHash, err
}
ctx := context.Background()
response, err := l.client.AddInvoice(ctx, invoice)
if err != nil {
log.Errorf("Error adding invoice: %v", err)
return "", lntypes.ZeroHash, err
}
paymentHash, err := lntypes.MakeHash(response.RHash)
if err != nil {
log.Errorf("Error parsing payment hash: %v", err)
return "", lntypes.ZeroHash, err
}
return response.PaymentRequest, paymentHash, nil
}
// VerifyInvoiceStatus checks that an invoice identified by a payment
// hash has the desired status. To make sure we don't fail while the
// invoice update is still on its way, we try several times until either
// the desired status is set or the given timeout is reached.
//
// NOTE: This is part of the auth.InvoiceChecker interface.
func (l *LndChallenger) VerifyInvoiceStatus(hash lntypes.Hash,
state lnrpc.Invoice_InvoiceState, timeout time.Duration) error {
// Prevent the challenger to be shut down while we're still waiting for
// status updates.
l.wg.Add(1)
defer l.wg.Done()
var (
condWg sync.WaitGroup
doneChan = make(chan struct{})
timeoutReached bool
hasInvoice bool
invoiceState lnrpc.Invoice_InvoiceState
)
// First of all, spawn a goroutine that will signal us on timeout.
// Otherwise if a client subscribes to an update on an invoice that
// never arrives, and there is no other activity, it would block
// forever in the condition.
condWg.Add(1)
go func() {
defer condWg.Done()
select {
case <-doneChan:
case <-time.After(timeout):
case <-l.quit:
}
l.invoicesCond.L.Lock()
timeoutReached = true
l.invoicesCond.Broadcast()
l.invoicesCond.L.Unlock()
}()
// Now create the main goroutine that blocks until an update is received
// on the condition.
condWg.Add(1)
go func() {
defer condWg.Done()
l.invoicesCond.L.Lock()
// Block here until our condition is met or the allowed time is
// up. The Wait() will return whenever a signal is broadcast.
invoiceState, hasInvoice = l.invoiceStates[hash]
for !(hasInvoice && invoiceState == state) && !timeoutReached {
l.invoicesCond.Wait()
// The Wait() above has re-acquired the lock so we can
// safely access the states map.
invoiceState, hasInvoice = l.invoiceStates[hash]
}
// We're now done.
l.invoicesCond.L.Unlock()
close(doneChan)
}()
// Wait until we're either done or timed out.
condWg.Wait()
// Interpret the result so we can return a more descriptive error than
// just "failed".
switch {
case !hasInvoice:
return fmt.Errorf("no active or settled invoice found for "+
"hash=%v", hash)
case invoiceState != state:
return fmt.Errorf("invoice status not correct before timeout, "+
"hash=%v, status=%v", hash, invoiceState)
default:
return nil
}
}
// invoiceIrrelevant returns true if an invoice is nil, canceled or non-settled
// and expired.
func invoiceIrrelevant(invoice *lnrpc.Invoice) bool {
if invoice == nil || invoice.State == lnrpc.Invoice_CANCELED {
return true
}
creation := time.Unix(invoice.CreationDate, 0)
expiration := creation.Add(time.Duration(invoice.Expiry) * time.Second)
expired := time.Now().After(expiration)
notSettled := invoice.State == lnrpc.Invoice_OPEN ||
invoice.State == lnrpc.Invoice_ACCEPTED
return expired && notSettled
}