forked from lightninglabs/aperture
-
Notifications
You must be signed in to change notification settings - Fork 0
/
challenger_test.go
253 lines (214 loc) · 6.63 KB
/
challenger_test.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
package aperture
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
var (
defaultTimeout = 100 * time.Millisecond
)
type invoiceStreamMock struct {
lnrpc.Lightning_SubscribeInvoicesClient
updateChan chan *lnrpc.Invoice
errChan chan error
quit chan struct{}
}
func (i *invoiceStreamMock) Recv() (*lnrpc.Invoice, error) {
select {
case msg := <-i.updateChan:
return msg, nil
case err := <-i.errChan:
return nil, err
case <-i.quit:
return nil, context.Canceled
}
}
type mockInvoiceClient struct {
invoices []*lnrpc.Invoice
updateChan chan *lnrpc.Invoice
errChan chan error
quit chan struct{}
lastAddIndex uint64
}
// ListInvoices returns a paginated list of all invoices known to lnd.
func (m *mockInvoiceClient) ListInvoices(_ context.Context,
_ *lnrpc.ListInvoiceRequest,
_ ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error) {
return &lnrpc.ListInvoiceResponse{
Invoices: m.invoices,
}, nil
}
// SubscribeInvoices subscribes to updates on invoices.
func (m *mockInvoiceClient) SubscribeInvoices(_ context.Context,
in *lnrpc.InvoiceSubscription, _ ...grpc.CallOption) (
lnrpc.Lightning_SubscribeInvoicesClient, error) {
m.lastAddIndex = in.AddIndex
return &invoiceStreamMock{
updateChan: m.updateChan,
errChan: m.errChan,
quit: m.quit,
}, nil
}
// AddInvoice adds a new invoice to lnd.
func (m *mockInvoiceClient) AddInvoice(_ context.Context, in *lnrpc.Invoice,
_ ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) {
m.invoices = append(m.invoices, in)
return &lnrpc.AddInvoiceResponse{
RHash: in.RHash,
PaymentRequest: in.PaymentRequest,
AddIndex: uint64(len(m.invoices) - 1),
}, nil
}
func (m *mockInvoiceClient) stop() {
close(m.quit)
}
func newChallenger() (*LndChallenger, *mockInvoiceClient, chan error) {
mockClient := &mockInvoiceClient{
updateChan: make(chan *lnrpc.Invoice),
errChan: make(chan error, 1),
quit: make(chan struct{}),
}
genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) {
return newInvoice(lntypes.ZeroHash, 99, lnrpc.Invoice_OPEN),
nil
}
invoicesMtx := &sync.Mutex{}
mainErrChan := make(chan error)
return &LndChallenger{
client: mockClient,
genInvoiceReq: genInvoiceReq,
invoiceStates: make(map[lntypes.Hash]lnrpc.Invoice_InvoiceState),
quit: make(chan struct{}),
invoicesMtx: invoicesMtx,
invoicesCond: sync.NewCond(invoicesMtx),
errChan: mainErrChan,
}, mockClient, mainErrChan
}
func newInvoice(hash lntypes.Hash, addIndex uint64,
state lnrpc.Invoice_InvoiceState) *lnrpc.Invoice {
return &lnrpc.Invoice{
PaymentRequest: "foo",
RHash: hash[:],
AddIndex: addIndex,
State: state,
CreationDate: time.Now().Unix(),
Expiry: 10,
}
}
func TestLndChallenger(t *testing.T) {
t.Parallel()
// First of all, test that the NewLndChallenger doesn't allow a nil
// invoice generator function.
errChan := make(chan error)
_, err := NewLndChallenger(nil, nil, errChan)
require.Error(t, err)
// Now mock the lnd backend and create a challenger instance that we can
// test.
c, invoiceMock, mainErrChan := newChallenger()
// Creating a new challenge should add an invoice to the lnd backend.
req, hash, err := c.NewChallenge(1337)
require.NoError(t, err)
require.Equal(t, "foo", req)
require.Equal(t, lntypes.ZeroHash, hash)
require.Equal(t, 1, len(invoiceMock.invoices))
require.Equal(t, uint64(0), invoiceMock.lastAddIndex)
// Now we already have an invoice in our lnd mock. When starting the
// challenger, we should have that invoice in the cache and a
// subscription that only starts at our faked addIndex.
err = c.Start()
require.NoError(t, err)
require.Equal(t, 1, len(c.invoiceStates))
require.Equal(t, lnrpc.Invoice_OPEN, c.invoiceStates[lntypes.ZeroHash])
require.Equal(t, uint64(99), invoiceMock.lastAddIndex)
require.NoError(t, c.VerifyInvoiceStatus(
lntypes.ZeroHash, lnrpc.Invoice_OPEN, defaultTimeout,
))
require.Error(t, c.VerifyInvoiceStatus(
lntypes.ZeroHash, lnrpc.Invoice_SETTLED, defaultTimeout,
))
// Next, let's send an update for a new invoice and make sure it's added
// to the map.
hash = lntypes.Hash{77, 88, 99}
invoiceMock.updateChan <- newInvoice(hash, 123, lnrpc.Invoice_SETTLED)
require.NoError(t, c.VerifyInvoiceStatus(
hash, lnrpc.Invoice_SETTLED, defaultTimeout,
))
require.Error(t, c.VerifyInvoiceStatus(
hash, lnrpc.Invoice_OPEN, defaultTimeout,
))
// Finally, create a bunch of invoices but only settle the first 5 of
// them. All others should get a failed invoice state after the timeout.
var (
numInvoices = 20
errors = make([]error, numInvoices)
wg sync.WaitGroup
)
for i := 0; i < numInvoices; i++ {
hash := lntypes.Hash{77, 88, 99, byte(i)}
invoiceMock.updateChan <- newInvoice(
hash, 1000+uint64(i), lnrpc.Invoice_OPEN,
)
// The verification will block for a certain time. But we want
// all checks to happen automatically to simulate many parallel
// requests. So we spawn a goroutine for each invoice check.
wg.Add(1)
go func(errIdx int, hash lntypes.Hash) {
defer wg.Done()
errors[errIdx] = c.VerifyInvoiceStatus(
hash, lnrpc.Invoice_SETTLED, defaultTimeout,
)
}(i, hash)
}
// With all 20 goroutines spinning and waiting for updates, we settle
// the first 5 invoices.
for i := 0; i < 5; i++ {
hash := lntypes.Hash{77, 88, 99, byte(i)}
invoiceMock.updateChan <- newInvoice(
hash, 1000+uint64(i), lnrpc.Invoice_SETTLED,
)
}
// Now wait for all checks to finish, then check that the last 15
// invoices timed out.
wg.Wait()
for i := 0; i < numInvoices; i++ {
if i < 5 {
require.NoError(t, errors[i])
} else {
require.Error(t, errors[i])
require.Contains(
t, errors[i].Error(),
"invoice status not correct before timeout",
)
}
}
// Finally test that if an error occurs in the invoice subscription the
// challenger reports it on the main error channel to cause a shutdown
// of aperture. The mock's error channel is buffered so we can send
// directly.
invoiceMock.errChan <- fmt.Errorf("an expected error")
select {
case err := <-mainErrChan:
require.Error(t, err)
// Make sure that the goroutine exited.
done := make(chan struct{})
go func() {
c.wg.Wait()
done <- struct{}{}
}()
select {
case <-done:
case <-time.After(defaultTimeout):
t.Fatalf("wait group didn't finish before timeout")
}
case <-time.After(defaultTimeout):
t.Fatalf("error not received on main chan before the timeout")
}
invoiceMock.stop()
c.Stop()
}