cnct+htlcswitch+invoices: report circuit key to invoice registry

Currently the invoice registry cannot tell apart the htlcs that pay to
an invoice. Because htlcs may also be replayed on startup, it isn't
possible to determine the total amount paid to an invoice.

This commit is a first step towards fixing that. It reports the circuit
keys of htlcs to the invoice registry, which forms the basis for
accurate invoice accounting.
This commit is contained in:
Joost Jager 2019-08-08 15:48:31 +02:00
parent 5871d69bde
commit 05e6b62cb2
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
9 changed files with 76 additions and 28 deletions

@ -498,9 +498,7 @@ func (c *ChannelArbitrator) supplementResolver(resolver ContractResolver,
return c.supplementSuccessResolver(r, htlcMap) return c.supplementSuccessResolver(r, htlcMap)
case *htlcIncomingContestResolver: case *htlcIncomingContestResolver:
return c.supplementSuccessResolver( return c.supplementIncomingContestResolver(r, htlcMap)
&r.htlcSuccessResolver, htlcMap,
)
case *htlcTimeoutResolver: case *htlcTimeoutResolver:
return c.supplementTimeoutResolver(r, htlcMap) return c.supplementTimeoutResolver(r, htlcMap)
@ -514,6 +512,30 @@ func (c *ChannelArbitrator) supplementResolver(resolver ContractResolver,
return nil return nil
} }
// supplementSuccessResolver takes a htlcIncomingContestResolver as it is
// restored from the log and fills in missing data from the htlcMap.
func (c *ChannelArbitrator) supplementIncomingContestResolver(
r *htlcIncomingContestResolver,
htlcMap map[wire.OutPoint]*channeldb.HTLC) error {
res := r.htlcResolution
htlcPoint := res.HtlcPoint()
htlc, ok := htlcMap[htlcPoint]
if !ok {
return errors.New(
"htlc for incoming contest resolver unavailable",
)
}
r.htlcAmt = htlc.Amt
r.circuitKey = channeldb.CircuitKey{
ChanID: c.cfg.ShortChanID,
HtlcID: htlc.HtlcIndex,
}
return nil
}
// supplementSuccessResolver takes a htlcSuccessResolver as it is restored from // supplementSuccessResolver takes a htlcSuccessResolver as it is restored from
// the log and fills in missing data from the htlcMap. // the log and fills in missing data from the htlcMap.
func (c *ChannelArbitrator) supplementSuccessResolver(r *htlcSuccessResolver, func (c *ChannelArbitrator) supplementSuccessResolver(r *htlcSuccessResolver,

@ -27,6 +27,9 @@ type htlcIncomingContestResolver struct {
// successfully. // successfully.
htlcExpiry uint32 htlcExpiry uint32
// circuitKey describes the incoming htlc that is being resolved.
circuitKey channeldb.CircuitKey
// htlcSuccessResolver is the inner resolver that may be utilized if we // htlcSuccessResolver is the inner resolver that may be utilized if we
// learn of the preimage. // learn of the preimage.
htlcSuccessResolver htlcSuccessResolver
@ -166,7 +169,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
// identical to HTLC resolution in the link. // identical to HTLC resolution in the link.
event, err := h.Registry.NotifyExitHopHtlc( event, err := h.Registry.NotifyExitHopHtlc(
h.payHash, h.htlcAmt, h.htlcExpiry, currentHeight, h.payHash, h.htlcAmt, h.htlcExpiry, currentHeight,
hodlChan, nil, h.circuitKey, hodlChan, nil,
) )
switch err { switch err {
case channeldb.ErrInvoiceNotFound: case channeldb.ErrInvoiceNotFound:

@ -22,7 +22,7 @@ type Registry interface {
// the resolution is sent on the passed in hodlChan later. // the resolution is sent on the passed in hodlChan later.
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) eob []byte) (*invoices.HodlEvent, error)
// HodlUnsubscribeAll unsubscribes from all hodl events. // HodlUnsubscribeAll unsubscribes from all hodl events.

@ -23,7 +23,8 @@ type mockRegistry struct {
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*invoices.HodlEvent, error) { circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) {
r.notifyChan <- notifyExitHopData{ r.notifyChan <- notifyExitHopData{
hodlChan: hodlChan, hodlChan: hodlChan,

@ -28,7 +28,7 @@ type InvoiceDatabase interface {
// for decoding purposes. // for decoding purposes.
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) eob []byte) (*invoices.HodlEvent, error)
// CancelInvoice attempts to cancel the invoice corresponding to the // CancelInvoice attempts to cancel the invoice corresponding to the

@ -2878,9 +2878,14 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
// receive back a resolution event. // receive back a resolution event.
invoiceHash := lntypes.Hash(pd.RHash) invoiceHash := lntypes.Hash(pd.RHash)
circuitKey := channeldb.CircuitKey{
ChanID: l.ShortChanID(),
HtlcID: pd.HtlcIndex,
}
event, err := l.cfg.Registry.NotifyExitHopHtlc( event, err := l.cfg.Registry.NotifyExitHopHtlc(
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow), invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
l.hodlQueue.ChanIn(), eob, circuitKey, l.hodlQueue.ChanIn(), eob,
) )
switch err { switch err {

@ -793,10 +793,11 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash, func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32, amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*invoices.HodlEvent, error) { circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) {
event, err := i.registry.NotifyExitHopHtlc( event, err := i.registry.NotifyExitHopHtlc(
rhash, amt, expiry, currentHeight, hodlChan, eob, rhash, amt, expiry, currentHeight, circuitKey, hodlChan, eob,
) )
if err != nil { if err != nil {
return nil, err return nil, err

@ -484,14 +484,15 @@ func (i *InvoiceRegistry) checkHtlcParameters(invoice *channeldb.Invoice,
// prevent deadlock. // prevent deadlock.
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32, amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*HodlEvent, error) { circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*HodlEvent, error) {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()
debugLog := func(s string) { debugLog := func(s string) {
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v", log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v",
rHash[:], s, amtPaid, expiry) rHash[:], s, amtPaid, expiry, circuitKey)
} }
// If this isn't a debug invoice, then we'll attempt to settle an // If this isn't a debug invoice, then we'll attempt to settle an

@ -64,6 +64,15 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
} }
} }
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. // TestSettleInvoice tests settling of an invoice and related notifications.
func TestSettleInvoice(t *testing.T) { func TestSettleInvoice(t *testing.T) {
registry, cleanup := newTestContext(t) registry, cleanup := newTestContext(t)
@ -121,7 +130,8 @@ func TestSettleInvoice(t *testing.T) {
// Settle invoice with a slightly higher amount. // Settle invoice with a slightly higher amount.
amtPaid := lnwire.MilliSatoshi(100500) amtPaid := lnwire.MilliSatoshi(100500)
_, err = registry.NotifyExitHopHtlc( _, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -153,10 +163,11 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("no update received") t.Fatal("no update received")
} }
// Try to settle again. We need this idempotent behaviour after a // Try to settle again with the same htlc id. We need this idempotent
// restart. // behaviour after a restart.
event, err := registry.NotifyExitHopHtlc( event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
@ -165,12 +176,12 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("expected settle event") t.Fatal("expected settle event")
} }
// Try to settle again with a higher amount. This payment should also be // Try to settle again with a new higher-valued htlc. This payment
// accepted, to prevent any change in behaviour for a paid invoice that // should also be accepted, to prevent any change in behaviour for a
// may open up a probe vector. // paid invoice that may open up a probe vector.
event, err = registry.NotifyExitHopHtlc( event, err = registry.NotifyExitHopHtlc(
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight, hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
hodlChan, nil, getCircuitKey(1), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
@ -183,7 +194,7 @@ func TestSettleInvoice(t *testing.T) {
// would have failed if it were the first payment. // would have failed if it were the first payment.
event, err = registry.NotifyExitHopHtlc( event, err = registry.NotifyExitHopHtlc(
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight, hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
hodlChan, nil, getCircuitKey(2), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
@ -306,7 +317,8 @@ func TestCancelInvoice(t *testing.T) {
// succeed. // succeed.
hodlChan := make(chan interface{}) hodlChan := make(chan interface{})
event, err := registry.NotifyExitHopHtlc( event, err := registry.NotifyExitHopHtlc(
hash, amt, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatal("expected settlement of a canceled invoice to succeed") t.Fatal("expected settlement of a canceled invoice to succeed")
@ -382,7 +394,8 @@ func TestHoldInvoice(t *testing.T) {
// NotifyExitHopHtlc without a preimage present in the invoice registry // NotifyExitHopHtlc without a preimage present in the invoice registry
// should be possible. // should be possible.
event, err := registry.NotifyExitHopHtlc( event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("expected settle to succeed but got %v", err) t.Fatalf("expected settle to succeed but got %v", err)
@ -393,7 +406,8 @@ func TestHoldInvoice(t *testing.T) {
// Test idempotency. // Test idempotency.
event, err = registry.NotifyExitHopHtlc( event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("expected settle to succeed but got %v", err) t.Fatalf("expected settle to succeed but got %v", err)
@ -406,7 +420,7 @@ func TestHoldInvoice(t *testing.T) {
// is a replay. // is a replay.
event, err = registry.NotifyExitHopHtlc( event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight+10, hash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
getCircuitKey(0), hodlChan, getCircuitKey(0), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("expected settle to succeed but got %v", err) t.Fatalf("expected settle to succeed but got %v", err)
@ -420,7 +434,7 @@ func TestHoldInvoice(t *testing.T) {
// doesn't track individual htlcs it is accepted. // doesn't track individual htlcs it is accepted.
event, err = registry.NotifyExitHopHtlc( event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, 1, testCurrentHeight, hash, amtPaid, 1, testCurrentHeight,
getCircuitKey(1), hodlChan, getCircuitKey(1), hodlChan, nil,
) )
if err != nil { if err != nil {
t.Fatalf("expected settle to succeed but got %v", err) t.Fatalf("expected settle to succeed but got %v", err)
@ -520,7 +534,8 @@ func TestUnknownInvoice(t *testing.T) {
hodlChan := make(chan interface{}) hodlChan := make(chan interface{})
amt := lnwire.MilliSatoshi(100000) amt := lnwire.MilliSatoshi(100000)
_, err := registry.NotifyExitHopHtlc( _, err := registry.NotifyExitHopHtlc(
hash, amt, testHtlcExpiry, testCurrentHeight, hodlChan, nil, hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
) )
if err != channeldb.ErrInvoiceNotFound { if err != channeldb.ErrInvoiceNotFound {
t.Fatal("expected invoice not found error") t.Fatal("expected invoice not found error")