From a1fd2dbbd24d71cc0cc5d7e8544214435a029bbc Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 3 Jun 2020 12:00:13 +0200 Subject: [PATCH] accounting: add regular and circular payment entries --- accounting/conversions.go | 5 ++ accounting/docs.md | 30 +++++++++++ accounting/entries.go | 74 ++++++++++++++++++++++++++ accounting/entries_test.go | 103 +++++++++++++++++++++++++++++++++++++ accounting/report.go | 8 +++ 5 files changed, 220 insertions(+) diff --git a/accounting/conversions.go b/accounting/conversions.go index 2445782..764b0ed 100644 --- a/accounting/conversions.go +++ b/accounting/conversions.go @@ -24,6 +24,11 @@ func invertedSatsToMsats(sats int64) int64 { return satsToMsat(sats) * -1 } +// invertMsat flips the sign value of a msat value. +func invertMsat(msat int64) int64 { + return msat * -1 +} + // getConversion is a helper function which queries coincap for a relevant set // of price data and returns a convert function which can be used to get // individual price points from this data. diff --git a/accounting/docs.md b/accounting/docs.md index 3a8d215..1a00b23 100644 --- a/accounting/docs.md +++ b/accounting/docs.md @@ -103,6 +103,36 @@ Circular receipts record instances where we have paid one of our own invoices. - Reference: The preimage of the invoice. - Note: Optionally set if the invoice had a memo attached, was overpaid, or was a keysend. +### Payment +Payments off chain represent payments made via the Lightning Network. + +- Amount: The amount in millisatoshis that we paid, excluding the off chain fees paid. +- TxID: The payment hash. +- Reference: Unique payment ID: Payment hash. +- Note: The preimage for the payment, which serves as proof of payment. + +### Fee +- Amount: The amount in millisatoshis that was paid in off chain fees. +- TxID: The payment hash. +- Reference: Unique payment ID: Payment hash: -1. +- Note: A note indicating the number of htlcs the payment was paid over. + +### Circular Payment +Circular payments represent payments made to our own node to rebalance channels. These payments are paid from our node to one of our own invoices. + +- Amount: The amount that was rebalanced. +- TxID: The payment hash. +- Reference: Unique payment ID: Payment hash. +- Note: The preimage for the payment, which serves as proof of payment. + +### Circular Payment Fee +Circular payment fees represent the fees we paid to loop a circular payment to ourselves. + +- Amount: The amount that was paid in off chain fees. +- TxID: The payment hash. +- Reference: Unique payment ID: Payment hash: -1. +- Note: A note indicating the number of htlcs the payment was paid over. + ### Forwards A forward represents a payment that arrives at our node on an incoming channel and is forwarded out on an outgoing channel in exchange for fees. The forward itself does not changes our balance, since it just shifts funds over our channels. We include forwarding entries with zero balances for completeness. Forwarding fee entries reflect the increase in our holdings from the fee we are paid. diff --git a/accounting/entries.go b/accounting/entries.go index 9434b11..0439b82 100644 --- a/accounting/entries.go +++ b/accounting/entries.go @@ -242,6 +242,80 @@ func invoiceEntry(invoice *lnrpc.Invoice, circularReceipt bool, ) } +// paymentReference produces a unique reference for a payment. Since payment +// hash is not guaranteed to be unique, we use the payments unique sequence +// number and its hash. +func paymentReference(sequenceNumber uint64, paymentHash string) string { + return fmt.Sprintf("%v:%v", sequenceNumber, paymentHash) +} + +// paymentNote creates a note for payments from our node. +func paymentNote(preimage string) string { + return fmt.Sprintf("Preimage: %v", preimage) +} + +// paymentFeeNote creates a note for a payment fee entry. +func paymentFeeNote(htlcs []*lnrpc.HTLCAttempt) string { + return fmt.Sprintf("Settled with: %v htlc(s)", len(htlcs)) +} + +// paymentEntry creates an entry for an off chain payment, including fee entries +// where required. +func paymentEntry(payment settledPayment, paidToSelf bool, + convert msatToFiat) ([]*HarmonyEntry, error) { + + // It is possible to make a payment to ourselves as part of a circular + // rebalance which is operationally used to shift funds between + // channels. For these payment types, we lose balance from fees, but do + // not change our balance from the actual payment because it is paid + // back to ourselves. + var ( + paymentType = EntryTypePayment + feeType = EntryTypeFee + ) + + // If we made the payment to ourselves, we set special entry types, + // since the payment amount did not actually affect our balance. + if paidToSelf { + paymentType = EntryTypeCircularPayment + feeType = EntryTypeCircularPaymentFee + } + + note := paymentNote(payment.PaymentPreimage) + ref := paymentReference(payment.PaymentIndex, payment.PaymentHash) + + // Payment values are expressed as positive values over rpc, but they + // decrease our balance so we flip our value to a negative one. + amt := invertMsat(payment.ValueMsat) + + paymentEntry, err := newHarmonyEntry( + payment.settleTime.Unix(), amt, paymentType, + payment.PaymentHash, ref, note, false, convert, + ) + if err != nil { + return nil, err + } + + // If we paid no fees (possible for payments to our direct peer), then + // we just return the payment entry. + if payment.FeeMsat == 0 { + return []*HarmonyEntry{paymentEntry}, nil + } + + feeNote := paymentFeeNote(payment.Htlcs) + feeRef := feeReference(ref) + feeAmt := invertMsat(payment.FeeMsat) + + feeEntry, err := newHarmonyEntry( + payment.settleTime.Unix(), feeAmt, feeType, + payment.PaymentHash, feeRef, feeNote, false, convert, + ) + if err != nil { + return nil, err + } + return []*HarmonyEntry{paymentEntry, feeEntry}, nil +} + // forwardTxid provides a best effort txid using incoming and outgoing channel // ID paired with timestamp in an effort to make txid unique per htlc forwarded. // This is not used as a reference because we could theoretically have duplicate diff --git a/accounting/entries_test.go b/accounting/entries_test.go index 6a34ac0..8d6cc5e 100644 --- a/accounting/entries_test.go +++ b/accounting/entries_test.go @@ -120,6 +120,33 @@ var ( IsKeysend: true, } + paymentTime = 1590399649 + + paymentHash = "11f414479f0a0c2762492c71c58dded5dce99d56d65c3fa523f73513605bebb3" + + paymentPreimage = "adfef20b24152accd4ed9a05257fb77203d90a8bbbe6d4069a75c5320f0538d9" + + paymentMsat = 30000 + + paymentFeeMsat = 45 + + paymentIndex = 33 + + payment = &lnrpc.Payment{ + PaymentHash: paymentHash, + PaymentPreimage: paymentPreimage, + ValueMsat: int64(paymentMsat), + Status: lnrpc.Payment_SUCCEEDED, + FeeMsat: int64(paymentFeeMsat), + Htlcs: []*lnrpc.HTLCAttempt{{}}, + PaymentIndex: uint64(paymentIndex), + } + + settledPmt = settledPayment{ + Payment: payment, + settleTime: time.Unix(int64(paymentTime), 0), + } + forwardTs uint64 = 1590578022 forwardChanIn uint64 = 130841883770880 @@ -501,6 +528,82 @@ func TestInvoiceEntry(t *testing.T) { } } +// TestPaymentEntry tests creation of payment entries for circular rebalances +// and regular payments. +func TestPaymentEntry(t *testing.T) { + // getEntries is a helper function which returns our expected entries + // based on whether we are testing a payment to ourselves or not. + getEntries := func(toSelf bool) []*HarmonyEntry { + mockFiat, _ := mockConvert(int64(paymentMsat), 0) + paymentRef := paymentReference( + uint64(paymentIndex), paymentHash, + ) + + paymentEntry := &HarmonyEntry{ + Timestamp: time.Unix(int64(paymentTime), 0), + Amount: lnwire.MilliSatoshi(paymentMsat), + FiatValue: mockFiat, + TxID: paymentHash, + Reference: paymentRef, + Note: paymentNote(paymentPreimage), + Type: EntryTypePayment, + OnChain: false, + Credit: false, + } + + feeFiat, _ := mockConvert(int64(paymentFeeMsat), 0) + feeEntry := &HarmonyEntry{ + Timestamp: time.Unix(int64(paymentTime), 0), + Amount: lnwire.MilliSatoshi(paymentFeeMsat), + FiatValue: feeFiat, + TxID: paymentHash, + Reference: feeReference(paymentRef), + Note: paymentFeeNote(payment.Htlcs), + Type: EntryTypeFee, + OnChain: false, + Credit: false, + } + + if toSelf { + paymentEntry.Type = EntryTypeCircularPayment + feeEntry.Type = EntryTypeCircularPaymentFee + } + + return []*HarmonyEntry{paymentEntry, feeEntry} + } + + tests := []struct { + name string + toSelf bool + }{ + { + name: "regular payment", + toSelf: false, + }, + { + name: "to self", + toSelf: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + entries, err := paymentEntry( + settledPmt, test.toSelf, mockConvert, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expectedEntries := getEntries(test.toSelf) + + require.Equal(t, expectedEntries, entries) + }) + } +} + // TestForwardingEntry tests creation of a forwarding and forwarding fee entry. func TestForwardingEntry(t *testing.T) { entries, err := forwardingEntry(fwdEntry, mockConvert) diff --git a/accounting/report.go b/accounting/report.go index 2d3ed8e..0f2b172 100644 --- a/accounting/report.go +++ b/accounting/report.go @@ -136,4 +136,12 @@ const ( // EntryTypeForwardFee represents the fees we earned forwarding a // payment. EntryTypeForwardFee + + // EntryTypeCircularPayment represents an operational payment which + // we pay to ourselves to rebalance channels. + EntryTypeCircularPayment + + // EntryTypeCircularPaymentFee represents a the fees paid on an + // operational payment paid to ourselves to rebalance channels. + EntryTypeCircularPaymentFee )