invoices: replay awareness

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.
This commit is contained in:
Joost Jager committed 2019-09-04 19:20:31 +02:00
1 parent 53eea09b63
commit d6d9ec6aa5
5 files changed
+188 -104

No files matched your search

+16 -25
View File
@@ -10,26 +10,6 @@ import (
"github.com/lightningnetwork/lnd/lnwire"
)
var (
testCircuitKey = CircuitKey{
ChanID: lnwire.ShortChannelID{
BlockHeight: 1, TxIndex: 2, TxPosition: 3,
},
HtlcID: 4,
}
testHtlcs = map[CircuitKey]*InvoiceHTLC{
testCircuitKey: {
State: HtlcStateCancelled,
AcceptTime: time.Unix(1, 0),
AcceptHeight: 100,
ResolveTime: time.Unix(2, 0),
Amt: 5200,
Expiry: 150,
},
}
)
func randInvoice(value lnwire.MilliSatoshi) (*Invoice, error) {
var pre [32]byte
if _, err := rand.Read(pre[:]); err != nil {
@@ -44,9 +24,8 @@ func randInvoice(value lnwire.MilliSatoshi) (*Invoice, error) {
PaymentPreimage: pre,
Value: value,
},
Htlcs: testHtlcs,
FinalCltvDelta: 50,
Expiry: 4000,
Htlcs: map[CircuitKey]*InvoiceHTLC{},
Expiry: 4000,
}
i.Memo = []byte("memo")
i.Receipt = []byte("receipt")
@@ -82,7 +61,7 @@ func TestInvoiceWorkflow(t *testing.T) {
// Use single second precision to avoid false positive test
// failures due to the monotonic time component.
CreationDate: time.Unix(time.Now().Unix(), 0),
Htlcs: testHtlcs,
Htlcs: map[CircuitKey]*InvoiceHTLC{},
}
fakeInvoice.Memo = []byte("memo")
fakeInvoice.Receipt = []byte("receipt")
@@ -383,6 +362,14 @@ func TestDuplicateSettleInvoice(t *testing.T) {
invoice.Terms.State = ContractSettled
invoice.AmtPaid = amt
invoice.SettleDate = dbInvoice.SettleDate
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
{}: {
Amt: amt,
AcceptTime: time.Unix(1, 0),
ResolveTime: time.Unix(1, 0),
State: HtlcStateSettled,
},
}
// We should get back the exact same invoice that we just inserted.
if !reflect.DeepEqual(dbInvoice, invoice) {
@@ -695,7 +682,11 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback {
update := &InvoiceUpdateDesc{
Preimage: invoice.Terms.PaymentPreimage,
State: ContractSettled,
AmtPaid: amt,
Htlcs: map[CircuitKey]*HtlcAcceptDesc{
{}: {
Amt: amt,
},
},
}
return update, nil
+72 -7
View File
@@ -276,14 +276,28 @@ type InvoiceHTLC struct {
State HtlcState
}
// HtlcAcceptDesc describes the details of a newly accepted htlc.
type HtlcAcceptDesc struct {
// AcceptHeight is the block height at which this htlc was accepted.
AcceptHeight int32
// Amt is the amount that is carried by this htlc.
Amt lnwire.MilliSatoshi
// Expiry is the expiry height of this htlc.
Expiry uint32
}
// InvoiceUpdateDesc describes the changes that should be applied to the
// invoice.
type InvoiceUpdateDesc struct {
// State is the new state that this invoice should progress to.
State ContractState
// AmtPaid is the updated amount that has been paid to this invoice.
AmtPaid lnwire.MilliSatoshi
// Htlcs describes the changes that need to be made to the invoice htlcs
// in the database. Htlc map entries with their value set should be
// added. If the map value is nil, the htlc should be cancelled.
Htlcs map[CircuitKey]*HtlcAcceptDesc
// Preimage must be set to the preimage when state is settled.
Preimage lntypes.Preimage
@@ -1196,9 +1210,52 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
return &invoice, err
}
// Update invoice state and amount.
// Update invoice state.
invoice.Terms.State = update.State
invoice.AmtPaid = update.AmtPaid
now := d.now()
// Update htlc set.
for key, htlcUpdate := range update.Htlcs {
htlc, ok := invoice.Htlcs[key]
// No update means the htlc needs to be cancelled.
if htlcUpdate == nil {
if !ok {
return nil, fmt.Errorf("unknown htlc %v", key)
}
if htlc.State == HtlcStateSettled {
return nil, fmt.Errorf("cannot cancel a " +
"settled htlc")
}
htlc.State = HtlcStateCancelled
htlc.ResolveTime = now
invoice.AmtPaid -= htlc.Amt
continue
}
// Add new htlc paying to the invoice.
if ok {
return nil, fmt.Errorf("htlc %v already exists", key)
}
htlc = &InvoiceHTLC{
Amt: htlcUpdate.Amt,
Expiry: htlcUpdate.Expiry,
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
AcceptTime: now,
}
if preUpdateState == ContractSettled {
htlc.State = HtlcStateSettled
htlc.ResolveTime = now
} else {
htlc.State = HtlcStateAccepted
}
invoice.Htlcs[key] = htlc
invoice.AmtPaid += htlc.Amt
}
// If invoice moved to the settled state, update settle index and settle
// time.
@@ -1210,9 +1267,17 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
}
invoice.Terms.PaymentPreimage = update.Preimage
err := setSettleFields(
settleIndex, invoiceNum, &invoice, d.now(),
)
// Settle all accepted htlcs.
for _, htlc := range invoice.Htlcs {
if htlc.State != HtlcStateAccepted {
continue
}
htlc.State = HtlcStateSettled
htlc.ResolveTime = now
}
err := setSettleFields(settleIndex, invoiceNum, &invoice, now)
if err != nil {
return nil, err
}