d6d9ec6aa5
Previously the invoice registry wasn't aware of replayed htlcs. This was dealt with by keeping the invoice accept/settle logic idempotent, so that a replay wouldn't have an effect. This mechanism has two limitations: 1. No accurate tracking of the total amount paid to an invoice. The total amount couldn't just be increased with every htlc received, because it could be a replay which would lead to counting the htlc amount multiple times. Therefore the total amount was set to the amount of the first htlc that was received, even though there may have been multiple htlcs paying to the invoice. 2. Impossible to check htlc expiry consistently for hodl invoices. When an htlc is new, its expiry needs to be checked against the invoice cltv delta. But for a replay, that check must be skipped. The htlc was accepted in time, the invoice was moved to the accepted state and a replay some blocks later shouldn't lead to that htlc being cancelled. Because the invoice registry couldn't recognize replays, it stopped checking htlc expiry heights when the invoice reached the accepted state. This prevents hold htlcs from being cancelled after a restart. But unfortunately this also caused additional htlcs to be accepted on an already accepted invoice without their expiry being checked. In this commit, the invoice registry starts to persistently track htlcs so that replays can be recognized. For replays, an htlc resolution action is returned early. This fixes both limitations mentioned above.
540 lines
14 KiB
Go
540 lines
14 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"
|
|
)
|
|
|
|
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)
|
|
)
|
|
|
|
var (
|
|
testInvoice = &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: preimage,
|
|
Value: lnwire.MilliSatoshi(100000),
|
|
},
|
|
}
|
|
)
|
|
|
|
func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
|
|
cdb, cleanup, err := newDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Instantiate and start the invoice registry.
|
|
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
|
|
|
err = registry.Start()
|
|
if err != nil {
|
|
cleanup()
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return registry, func() {
|
|
registry.Stop()
|
|
cleanup()
|
|
}
|
|
}
|
|
|
|
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) {
|
|
registry, cleanup := newTestContext(t)
|
|
defer cleanup()
|
|
|
|
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.
|
|
addIdx, err := 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.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
update.Terms.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.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
newInvoice.Terms.State)
|
|
}
|
|
case <-time.After(testTimeout):
|
|
t.Fatal("no update received")
|
|
}
|
|
|
|
hodlChan := make(chan interface{}, 1)
|
|
|
|
// Settle invoice with a slightly higher amount.
|
|
amtPaid := lnwire.MilliSatoshi(100500)
|
|
_, err = registry.NotifyExitHopHtlc(
|
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(0), hodlChan, nil,
|
|
)
|
|
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.Terms.State != channeldb.ContractSettled {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
update.Terms.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.Terms.State != channeldb.ContractSettled {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
settledInvoice.Terms.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 := registry.NotifyExitHopHtlc(
|
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(0), hodlChan, nil,
|
|
)
|
|
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 = registry.NotifyExitHopHtlc(
|
|
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(1), hodlChan, nil,
|
|
)
|
|
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 = registry.NotifyExitHopHtlc(
|
|
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(2), hodlChan, nil,
|
|
)
|
|
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 := registry.LookupInvoice(hash)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inv.AmtPaid != amtPaid+amtPaid+600 {
|
|
t.Fatal("amount incorrect")
|
|
}
|
|
|
|
// Try to cancel.
|
|
err = 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) {
|
|
registry, cleanup := newTestContext(t)
|
|
defer cleanup()
|
|
|
|
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
|
defer allSubscriptions.Cancel()
|
|
|
|
// Try to cancel the not yet existing invoice. This should fail.
|
|
err := registry.CancelInvoice(hash)
|
|
if err != channeldb.ErrInvoiceNotFound {
|
|
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
|
|
}
|
|
|
|
// 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.
|
|
amt := lnwire.MilliSatoshi(100000)
|
|
_, err = 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.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf(
|
|
"expected state ContractOpen, but got %v",
|
|
update.Terms.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.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf(
|
|
"expected state ContractOpen, but got %v",
|
|
newInvoice.Terms.State,
|
|
)
|
|
}
|
|
case <-time.After(testTimeout):
|
|
t.Fatal("no update received")
|
|
}
|
|
|
|
// Cancel invoice.
|
|
err = 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.Terms.State != channeldb.ContractCanceled {
|
|
t.Fatalf(
|
|
"expected state ContractCanceled, but got %v",
|
|
update.Terms.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 = 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
|
|
// succeed.
|
|
hodlChan := make(chan interface{})
|
|
event, err := registry.NotifyExitHopHtlc(
|
|
hash, amt, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(0), hodlChan, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal("expected settlement of a canceled invoice to succeed")
|
|
}
|
|
|
|
if event.Preimage != nil {
|
|
t.Fatal("expected cancel hodl event")
|
|
}
|
|
}
|
|
|
|
// TestHoldInvoice tests settling of a hold invoice and related notifications.
|
|
func TestHoldInvoice(t *testing.T) {
|
|
defer timeout(t)()
|
|
|
|
cdb, cleanup, err := newDB()
|
|
defer cleanup()
|
|
|
|
// Instantiate and start the invoice 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.
|
|
invoice := &channeldb.Invoice{
|
|
Terms: channeldb.ContractTerm{
|
|
PaymentPreimage: channeldb.UnknownPreimage,
|
|
Value: lnwire.MilliSatoshi(100000),
|
|
},
|
|
}
|
|
|
|
_, err = registry.AddInvoice(invoice, 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.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
update.Terms.State)
|
|
}
|
|
|
|
// We expect a new invoice notification to be sent out.
|
|
newInvoice := <-allSubscriptions.NewInvoices
|
|
if newInvoice.Terms.State != channeldb.ContractOpen {
|
|
t.Fatalf("expected state ContractOpen, but got %v",
|
|
newInvoice.Terms.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, nil,
|
|
)
|
|
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, nil,
|
|
)
|
|
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, nil,
|
|
)
|
|
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, nil,
|
|
)
|
|
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 cancelled")
|
|
}
|
|
|
|
// 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.Terms.State != channeldb.ContractAccepted {
|
|
t.Fatalf("expected state ContractAccepted, but got %v",
|
|
update.Terms.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")
|
|
}
|
|
|
|
// We expect a settled notification to be sent out for both all and
|
|
// single invoice subscribers.
|
|
settledInvoice := <-allSubscriptions.SettledInvoices
|
|
if settledInvoice.Terms.State != channeldb.ContractSettled {
|
|
t.Fatalf("expected state ContractSettled, but got %v",
|
|
settledInvoice.Terms.State)
|
|
}
|
|
if settledInvoice.AmtPaid != amtPaid {
|
|
t.Fatalf("expected amount to be %v, but got %v",
|
|
amtPaid, settledInvoice.AmtPaid)
|
|
}
|
|
|
|
update = <-subscription.Updates
|
|
if update.Terms.State != channeldb.ContractSettled {
|
|
t.Fatalf("expected state ContractSettled, but got %v",
|
|
update.Terms.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")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
registry, cleanup := newTestContext(t)
|
|
defer cleanup()
|
|
|
|
// Notify arrival of a new htlc paying to this invoice. This should
|
|
// succeed.
|
|
hodlChan := make(chan interface{})
|
|
amt := lnwire.MilliSatoshi(100000)
|
|
_, err := registry.NotifyExitHopHtlc(
|
|
hash, amt, testHtlcExpiry, testCurrentHeight,
|
|
getCircuitKey(0), hodlChan, nil,
|
|
)
|
|
if err != channeldb.ErrInvoiceNotFound {
|
|
t.Fatal("expected invoice not found error")
|
|
}
|
|
}
|