faraday/accounting/off_chain.go
Boris Nagaev 3431cd8c9e
accounting: add error messages for errors
Replaced "return err" cases with wrapped errors to attach the info which may be
useful when debugging.
2025-10-02 14:26:31 -03:00

252 lines
8.6 KiB
Go

package accounting
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
)
var (
// errNoHops is returned when we see a payment which has htlcs with no
// hops in its route.
errNoHops = errors.New("payment htlc has a route with zero hops")
// errNoHtlcs is returned when we encounter a legacy payment which is
// settled but has no htlcs recorded with it.
errNoHtlcs = errors.New("settled payment has no htlcs")
// errNoPaymentRequest is returned when a payment does not have a
// payment request.
errNoPaymentRequest = errors.New("no payment request present")
// errDifferentDuplicates is returned if we have payments with duplicate
// payment hashes where one is made to our own node and one is made to
// another node. This is unexpected because legacy duplicate payments in
// lnd reflect multiple attempts to pay the same invoice.
errDifferentDuplicates = errors.New("duplicate payments paid to " +
"different sources")
// errDuplicatesNotSupported is returned when we see payments with
// duplicate payment hashes. This was allowed in legacy versions of lnd,
// but is not supported for accounting purposes. Nodes with duplicates
// will be required to delete the duplicates or query over a range that
// excludes them.
errDuplicatesNotSupported = errors.New("duplicate payments not " +
"supported, query more recent timestamp to exclude duplicates")
)
// OffChainReport gets a report of off chain activity using live price data.
func OffChainReport(ctx context.Context, cfg *OffChainConfig) (Report, error) {
// Retrieve a function which can be used to query individual prices,
// or a no-op function if we do not want prices.
getPrice, err := getConversion(
ctx, cfg.StartTime, cfg.EndTime, cfg.DisableFiat,
cfg.PriceSourceCfg,
)
if err != nil {
return nil, fmt.Errorf("off-chain report: init conversion "+
"lookup for range [%v,%v) failed: %w", cfg.StartTime,
cfg.EndTime, err)
}
return offChainReportWithPrices(cfg, getPrice)
}
// offChainReportWithPrices produces off chain reports using the getPrice
// function provided. This allows testing of our report creation without calling
// the actual price API.
func offChainReportWithPrices(cfg *OffChainConfig, getPrice fiatPrice) (Report,
error) {
invoices, err := cfg.ListInvoices()
if err != nil {
return nil, fmt.Errorf("off-chain report: listing invoices "+
"failed: %w", err)
}
filteredInvoices := filterInvoices(cfg.StartTime, cfg.EndTime, invoices)
log.Infof("Retrieved: %v invoices, %v filtered", len(invoices),
len(filteredInvoices))
payments, err := cfg.ListPayments()
if err != nil {
return nil, fmt.Errorf("off-chain report: listing payments "+
"failed: %w", err)
}
preProcessed, err := preProcessPayments(payments, cfg.DecodePayReq)
if err != nil {
return nil, fmt.Errorf("off-chain report: preprocessing %d "+
"payments failed: %w", len(payments), err)
}
// Get a list of all the payments we made to ourselves.
paymentsToSelf, err := getCircularPayments(cfg.OwnPubKey, preProcessed)
if err != nil {
return nil, fmt.Errorf("off-chain report: identifying "+
"circular payments for node %v failed: %w",
cfg.OwnPubKey, err)
}
filteredPayments := filterPayments(
cfg.StartTime, cfg.EndTime, preProcessed,
)
if err := sanityCheckDuplicates(filteredPayments); err != nil {
return nil, fmt.Errorf("off-chain report: duplicate payment "+
"hashes detected in range [%v,%v): %w", cfg.StartTime,
cfg.EndTime, err)
}
log.Infof("Retrieved: %v payments, %v filtered, %v circular",
len(payments), len(filteredPayments), len(paymentsToSelf))
// Get all our forwards, we do not need to filter them because they
// are already supplied over the relevant range for our query.
forwards, err := cfg.ListForwards()
if err != nil {
return nil, fmt.Errorf("off-chain report: listing forwards "+
"failed: %w", err)
}
log.Infof("Retrieved: %v forwards", len(forwards))
u := entryUtils{
getFiat: getPrice,
customCategories: cfg.Categories,
}
return offChainReport(
filteredInvoices, filteredPayments, paymentsToSelf, forwards,
u,
)
}
// offChainReport produces an off chain transaction report. This function
// assumes that all entries passed into this function fall within our target
// date range, with the exception of payments to self which tracks payments
// that were made to ourselves for the sake of appropriately reporting the
// invoices they paid.
func offChainReport(invoices []lndclient.Invoice, payments []paymentInfo,
circularPayments map[string]bool, forwards []lndclient.ForwardingEvent,
utils entryUtils) (Report, error) {
var reports Report
for _, invoice := range invoices {
// If the invoice's payment hash is in our set of circular
// payments, we know that this payment was made to ourselves.
toSelf := circularPayments[invoice.Hash.String()]
entry, err := invoiceEntry(invoice, toSelf, utils)
if err != nil {
return nil, fmt.Errorf("invoice %v: creating entry "+
"failed: %w", invoice.Hash, err)
}
reports = append(reports, entry)
}
for _, payment := range payments {
// If the payment's payment request is in our set of circular
// payments, we know that this payment was made to ourselves.
toSelf := circularPayments[payment.Hash.String()]
entries, err := paymentEntry(payment, toSelf, utils)
if err != nil {
return nil, fmt.Errorf("payment %v: creating entries "+
"failed: %w", payment.Hash, err)
}
reports = append(reports, entries...)
}
for _, forward := range forwards {
entries, err := forwardingEntry(forward, utils)
if err != nil {
return nil, fmt.Errorf("forward at %v: creating "+
"entries failed: %w", forward.Timestamp, err)
}
reports = append(reports, entries...)
}
return reports, nil
}
// getCircularPayments returns a map of the payments that we made to our node.
// Note that this function does not only account for settled payments because it
// is possible that we made a payment to ourselves, settled the invoice and
// queried listPayments while the payment was still being settled back. If a
// payment does not have a record of its payment hash (which is the case for
// legacy payments, and payments that have just been dispatched), we log a
// warning but do not fail so that legacy nodes can still use this feature (they
// may just not detect old circular rebalances, which we document).
//
// To allow for legacy nodes that have payments with duplicate payment hashes,
// we allow for payments with duplicate payment hashes. We only fail if we
// detect payments with the same payment hash where one is to our node and one
// is not. This would make lookup in our circular payment map wrong for one of
// the payments (resulting in bugs) and is not expected, because duplicate
// payments are expected to reflect multiple attempts of the same payment.
func getCircularPayments(ourPubkey route.Vertex,
payments []paymentInfo) (map[string]bool, error) {
// Run through all payments and get those that were made to our own
// node. We identify these payments by payment hash so that we can
// identify associated invoices.
paymentsToSelf := make(map[string]bool)
for _, payment := range payments {
// Try to determine whether a payment is made to our own
// node by checking its htlcs. If we cannot get this information
// from our set of htlcs, we fallback to trying to decode our
// payment request. If the payment request is not present as
// well, we skip over this payment and log a warning (because
// legacy nodes will always have these payment present).
if payment.destination == nil {
log.Warnf("payment %v destination unknown",
payment.Hash)
continue
}
// Check whether the payment is made to our own node.
toSelf := bytes.Equal(ourPubkey[:], payment.destination[:])
// Before we add our entry to the map, we sanity check that if
// it has any duplicates, the value in the map is the same as
// the value we are about to add.
duplicateToSelf, ok := paymentsToSelf[payment.Hash.String()]
if ok && duplicateToSelf != toSelf {
return nil, errDifferentDuplicates
}
if toSelf {
paymentsToSelf[payment.Hash.String()] = toSelf
}
}
return paymentsToSelf, nil
}
// sanityCheckDuplicates checks that we have no payments with duplicate payment
// hashes. We do not support accounting for duplicate payments.
func sanityCheckDuplicates(payments []paymentInfo) error {
uniqueHashes := make(map[lntypes.Hash]bool, len(payments))
for _, payment := range payments {
_, ok := uniqueHashes[payment.Hash]
if ok {
return errDuplicatesNotSupported
}
uniqueHashes[payment.Hash] = true
}
return nil
}