mirror of
https://github.com/lightninglabs/aperture.git
synced 2025-12-18 01:24:19 +01:00
To make sure we can capture errors in the challenger's invoice subscription, we hand the main error channel to the challenger so it can report back errors on it.
226 lines
5.9 KiB
Go
226 lines
5.9 KiB
Go
package aperture
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
var (
|
|
defaultTimeout = 20 * 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, _ := 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",
|
|
)
|
|
}
|
|
}
|
|
|
|
invoiceMock.stop()
|
|
c.Stop()
|
|
}
|