lnd.xprv/invoices/invoiceregistry_test.go
Joost Jager fa010de548
invoices/test: add test context
This commit adds a test context for invoice registry and additionally
passed in a payload object to NotifyExitHopHtlc. This makes the test
match the reality better where a payload is always provided.
2019-12-04 14:51:32 +01:00

672 lines
17 KiB
Go

package invoices
import (
"io/ioutil"
"os"
"testing"
"time"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
)
var (
testTimeout = 5 * time.Second
preimage = lntypes.Preimage{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
}
hash = preimage.Hash()
testHtlcExpiry = uint32(5)
testInvoiceCltvDelta = uint32(4)
testFinalCltvRejectDelta = int32(4)
testCurrentHeight = int32(1)
testFeatures = lnwire.NewFeatureVector(
nil, lnwire.Features,
)
testPayload = &mockPayload{}
)
var (
testInvoiceAmt = lnwire.MilliSatoshi(100000)
testInvoice = &channeldb.Invoice{
Terms: channeldb.ContractTerm{
PaymentPreimage: preimage,
Value: lnwire.MilliSatoshi(100000),
Features: testFeatures,
},
}
testHodlInvoice = &channeldb.Invoice{
Terms: channeldb.ContractTerm{
PaymentPreimage: channeldb.UnknownPreimage,
Value: testInvoiceAmt,
Features: testFeatures,
},
}
)
type testContext struct {
registry *InvoiceRegistry
cleanup func()
t *testing.T
}
func newTestContext(t *testing.T) *testContext {
cdb, cleanup, err := newDB()
if err != nil {
t.Fatal(err)
}
// Instantiate and start the invoice ctx.registry.
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
err = registry.Start()
if err != nil {
cleanup()
t.Fatal(err)
}
ctx := testContext{
registry: registry,
t: t,
cleanup: func() {
registry.Stop()
cleanup()
},
}
return &ctx
}
func getCircuitKey(htlcID uint64) channeldb.CircuitKey {
return channeldb.CircuitKey{
ChanID: lnwire.ShortChannelID{
BlockHeight: 1, TxIndex: 2, TxPosition: 3,
},
HtlcID: htlcID,
}
}
// TestSettleInvoice tests settling of an invoice and related notifications.
func TestSettleInvoice(t *testing.T) {
ctx := newTestContext(t)
defer ctx.cleanup()
allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
// Subscribe to the not yet existing invoice.
subscription, err := ctx.registry.SubscribeSingleInvoice(hash)
if err != nil {
t.Fatal(err)
}
defer subscription.Cancel()
if subscription.hash != hash {
t.Fatalf("expected subscription for provided hash")
}
// Add the invoice.
addIdx, err := ctx.registry.AddInvoice(testInvoice, hash)
if err != nil {
t.Fatal(err)
}
if addIdx != 1 {
t.Fatalf("expected addIndex to start with 1, but got %v",
addIdx)
}
// We expect the open state to be sent to the single invoice subscriber.
select {
case update := <-subscription.Updates:
if update.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
update.State)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect a new invoice notification to be sent out.
select {
case newInvoice := <-allSubscriptions.NewInvoices:
if newInvoice.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
newInvoice.State)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
hodlChan := make(chan interface{}, 1)
// Try to settle invoice with an htlc that expires too soon.
event, err := ctx.registry.NotifyExitHopHtlc(
hash, testInvoice.Terms.Value,
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
)
if err != nil {
t.Fatal(err)
}
if event.Preimage != nil {
t.Fatal("expected cancel event")
}
if event.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, event.AcceptHeight)
}
// Settle invoice with a slightly higher amount.
amtPaid := lnwire.MilliSatoshi(100500)
_, err = ctx.registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatal(err)
}
// We expect the settled state to be sent to the single invoice
// subscriber.
select {
case update := <-subscription.Updates:
if update.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractOpen, but got %v",
update.State)
}
if update.AmtPaid != amtPaid {
t.Fatal("invoice AmtPaid incorrect")
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect a settled notification to be sent out.
select {
case settledInvoice := <-allSubscriptions.SettledInvoices:
if settledInvoice.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractOpen, but got %v",
settledInvoice.State)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// Try to settle again with the same htlc id. We need this idempotent
// behaviour after a restart.
event, err = ctx.registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if event.Preimage == nil {
t.Fatal("expected settle event")
}
// Try to settle again with a new higher-valued htlc. This payment
// should also be accepted, to prevent any change in behaviour for a
// paid invoice that may open up a probe vector.
event, err = ctx.registry.NotifyExitHopHtlc(
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
getCircuitKey(1), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if event.Preimage == nil {
t.Fatal("expected settle event")
}
// Try to settle again with a lower amount. This should fail just as it
// would have failed if it were the first payment.
event, err = ctx.registry.NotifyExitHopHtlc(
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
getCircuitKey(2), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if event.Preimage != nil {
t.Fatal("expected cancel event")
}
// Check that settled amount is equal to the sum of values of the htlcs
// 0 and 1.
inv, err := ctx.registry.LookupInvoice(hash)
if err != nil {
t.Fatal(err)
}
if inv.AmtPaid != amtPaid+amtPaid+600 {
t.Fatal("amount incorrect")
}
// Try to cancel.
err = ctx.registry.CancelInvoice(hash)
if err != channeldb.ErrInvoiceAlreadySettled {
t.Fatal("expected cancelation of a settled invoice to fail")
}
// As this is a direct sette, we expect nothing on the hodl chan.
select {
case <-hodlChan:
t.Fatal("unexpected event")
default:
}
}
// TestCancelInvoice tests cancelation of an invoice and related notifications.
func TestCancelInvoice(t *testing.T) {
ctx := newTestContext(t)
defer ctx.cleanup()
allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
// Try to cancel the not yet existing invoice. This should fail.
err := ctx.registry.CancelInvoice(hash)
if err != channeldb.ErrInvoiceNotFound {
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
}
// Subscribe to the not yet existing invoice.
subscription, err := ctx.registry.SubscribeSingleInvoice(hash)
if err != nil {
t.Fatal(err)
}
defer subscription.Cancel()
if subscription.hash != hash {
t.Fatalf("expected subscription for provided hash")
}
// Add the invoice.
amt := lnwire.MilliSatoshi(100000)
_, err = ctx.registry.AddInvoice(testInvoice, hash)
if err != nil {
t.Fatal(err)
}
// We expect the open state to be sent to the single invoice subscriber.
select {
case update := <-subscription.Updates:
if update.State != channeldb.ContractOpen {
t.Fatalf(
"expected state ContractOpen, but got %v",
update.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect a new invoice notification to be sent out.
select {
case newInvoice := <-allSubscriptions.NewInvoices:
if newInvoice.State != channeldb.ContractOpen {
t.Fatalf(
"expected state ContractOpen, but got %v",
newInvoice.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// Cancel invoice.
err = ctx.registry.CancelInvoice(hash)
if err != nil {
t.Fatal(err)
}
// We expect the canceled state to be sent to the single invoice
// subscriber.
select {
case update := <-subscription.Updates:
if update.State != channeldb.ContractCanceled {
t.Fatalf(
"expected state ContractCanceled, but got %v",
update.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect no cancel notification to be sent to all invoice
// subscribers (backwards compatibility).
// Try to cancel again.
err = ctx.registry.CancelInvoice(hash)
if err != nil {
t.Fatal("expected cancelation of a canceled invoice to succeed")
}
// Notify arrival of a new htlc paying to this invoice. This should
// result in a cancel event.
hodlChan := make(chan interface{})
event, err := ctx.registry.NotifyExitHopHtlc(
hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatal("expected settlement of a canceled invoice to succeed")
}
if event.Preimage != nil {
t.Fatal("expected cancel hodl event")
}
if event.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, event.AcceptHeight)
}
}
// TestSettleHoldInvoice tests settling of a hold invoice and related
// notifications.
func TestSettleHoldInvoice(t *testing.T) {
defer timeout(t)()
cdb, cleanup, err := newDB()
if err != nil {
t.Fatal(err)
}
defer cleanup()
// Instantiate and start the invoice ctx.registry.
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
err = registry.Start()
if err != nil {
t.Fatal(err)
}
defer registry.Stop()
allSubscriptions := registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
// Subscribe to the not yet existing invoice.
subscription, err := registry.SubscribeSingleInvoice(hash)
if err != nil {
t.Fatal(err)
}
defer subscription.Cancel()
if subscription.hash != hash {
t.Fatalf("expected subscription for provided hash")
}
// Add the invoice.
_, err = registry.AddInvoice(testHodlInvoice, hash)
if err != nil {
t.Fatal(err)
}
// We expect the open state to be sent to the single invoice subscriber.
update := <-subscription.Updates
if update.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
update.State)
}
// We expect a new invoice notification to be sent out.
newInvoice := <-allSubscriptions.NewInvoices
if newInvoice.State != channeldb.ContractOpen {
t.Fatalf("expected state ContractOpen, but got %v",
newInvoice.State)
}
// Use slightly higher amount for accept/settle.
amtPaid := lnwire.MilliSatoshi(100500)
hodlChan := make(chan interface{}, 1)
// NotifyExitHopHtlc without a preimage present in the invoice registry
// should be possible.
event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("expected htlc to be held")
}
// Test idempotency.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("expected htlc to be held")
}
// Test replay at a higher height. We expect the same result because it
// is a replay.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("expected htlc to be held")
}
// Test a new htlc coming in that doesn't meet the final cltv delta
// requirement. It should be rejected.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, 1, testCurrentHeight,
getCircuitKey(1), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event == nil || event.Preimage != nil {
t.Fatalf("expected htlc to be canceled")
}
// We expect the accepted state to be sent to the single invoice
// subscriber. For all invoice subscribers, we don't expect an update.
// Those only get notified on settle.
update = <-subscription.Updates
if update.State != channeldb.ContractAccepted {
t.Fatalf("expected state ContractAccepted, but got %v",
update.State)
}
if update.AmtPaid != amtPaid {
t.Fatal("invoice AmtPaid incorrect")
}
// Settling with preimage should succeed.
err = registry.SettleHodlInvoice(preimage)
if err != nil {
t.Fatal("expected set preimage to succeed")
}
hodlEvent := (<-hodlChan).(HodlEvent)
if *hodlEvent.Preimage != preimage {
t.Fatal("unexpected preimage in hodl event")
}
if hodlEvent.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, event.AcceptHeight)
}
// We expect a settled notification to be sent out for both all and
// single invoice subscribers.
settledInvoice := <-allSubscriptions.SettledInvoices
if settledInvoice.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractSettled, but got %v",
settledInvoice.State)
}
if settledInvoice.AmtPaid != amtPaid {
t.Fatalf("expected amount to be %v, but got %v",
amtPaid, settledInvoice.AmtPaid)
}
update = <-subscription.Updates
if update.State != channeldb.ContractSettled {
t.Fatalf("expected state ContractSettled, but got %v",
update.State)
}
// Idempotency.
err = registry.SettleHodlInvoice(preimage)
if err != channeldb.ErrInvoiceAlreadySettled {
t.Fatalf("expected ErrInvoiceAlreadySettled but got %v", err)
}
// Try to cancel.
err = registry.CancelInvoice(hash)
if err == nil {
t.Fatal("expected cancelation of a settled invoice to fail")
}
}
// TestCancelHoldInvoice tests canceling of a hold invoice and related
// notifications.
func TestCancelHoldInvoice(t *testing.T) {
defer timeout(t)()
cdb, cleanup, err := newDB()
if err != nil {
t.Fatal(err)
}
defer cleanup()
// Instantiate and start the invoice ctx.registry.
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
err = registry.Start()
if err != nil {
t.Fatal(err)
}
defer registry.Stop()
// Add the invoice.
_, err = registry.AddInvoice(testHodlInvoice, hash)
if err != nil {
t.Fatal(err)
}
amtPaid := lnwire.MilliSatoshi(100000)
hodlChan := make(chan interface{}, 1)
// NotifyExitHopHtlc without a preimage present in the invoice registry
// should be possible.
event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("expected htlc to be held")
}
// Cancel invoice.
err = registry.CancelInvoice(hash)
if err != nil {
t.Fatal("cancel invoice failed")
}
hodlEvent := (<-hodlChan).(HodlEvent)
if hodlEvent.Preimage != nil {
t.Fatal("expected cancel hodl event")
}
// Offering the same htlc again at a higher height should still result
// in a rejection. The accept height is expected to be the original
// accept height.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event.Preimage != nil {
t.Fatalf("expected htlc to be canceled")
}
if event.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, event.AcceptHeight)
}
}
func newDB() (*channeldb.DB, func(), error) {
// First, create a temporary directory to be used for the duration of
// this test.
tempDirName, err := ioutil.TempDir("", "channeldb")
if err != nil {
return nil, nil, err
}
// Next, create channeldb for the first time.
cdb, err := channeldb.Open(tempDirName)
if err != nil {
os.RemoveAll(tempDirName)
return nil, nil, err
}
cleanUp := func() {
cdb.Close()
os.RemoveAll(tempDirName)
}
return cdb, cleanUp, nil
}
// TestUnknownInvoice tests that invoice registry returns an error when the
// invoice is unknown. This is to guard against returning a cancel hodl event
// for forwarded htlcs. In the link, NotifyExitHopHtlc is only called if we are
// the exit hop, but in htlcIncomingContestResolver it is called with forwarded
// htlc hashes as well.
func TestUnknownInvoice(t *testing.T) {
ctx := newTestContext(t)
defer ctx.cleanup()
// Notify arrival of a new htlc paying to this invoice. This should
// succeed.
hodlChan := make(chan interface{})
amt := lnwire.MilliSatoshi(100000)
_, err := ctx.registry.NotifyExitHopHtlc(
hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != channeldb.ErrInvoiceNotFound {
t.Fatal("expected invoice not found error")
}
}
type mockPayload struct {
mpp *record.MPP
}
func (p *mockPayload) MultiPath() *record.MPP {
return p.mpp
}