diff --git a/invoices/sql_store.go b/invoices/sql_store.go index a31d354fc..0628a5d54 100644 --- a/invoices/sql_store.go +++ b/invoices/sql_store.go @@ -1,6 +1,7 @@ package invoices import ( + "bytes" "context" "crypto/sha256" "database/sql" @@ -13,7 +14,6 @@ import ( "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/sqldb" @@ -100,12 +100,12 @@ type SQLInvoiceQueries interface { //nolint:interfacebloat FilterInvoicesReverse(ctx context.Context, arg sqlc.FilterInvoicesReverseParams) ([]sqlc.Invoice, error) - GetInvoice(ctx context.Context, - arg sqlc.GetInvoiceParams) ([]sqlc.Invoice, error) - GetInvoiceByHash(ctx context.Context, hash []byte) (sqlc.Invoice, error) + GetInvoiceByAddr(ctx context.Context, + paymentAddr []byte) (sqlc.Invoice, error) + GetInvoiceBySetID(ctx context.Context, setID []byte) ([]sqlc.Invoice, error) @@ -398,73 +398,76 @@ func getInvoiceByRef(ctx context.Context, return sqlc.Invoice{}, ErrInvoiceNotFound } - // If the reference is a hash only, we can look up the invoice directly - // by the payment hash which is faster. - if ref.IsHashOnly() { + // If the reference contains a payment hash we can look up the invoice + // directly by hash using the unique index, avoiding a full table scan. + // The hash alone uniquely identifies any invoice so additional fields + // in the ref (payment address, set ID) are not needed for the lookup. + if ref.PayHash() != nil { invoice, err := db.GetInvoiceByHash(ctx, ref.PayHash()[:]) if errors.Is(err, sql.ErrNoRows) { return sqlc.Invoice{}, ErrInvoiceNotFound } + if err != nil { + return sqlc.Invoice{}, fmt.Errorf("unable to fetch "+ + "invoice by hash: %w", err) + } - return invoice, err + // If the ref also specifies a payment address, verify it + // matches the invoice found by hash. A mismatch means the ref + // is equivocating — the hash points to one invoice and the + // address points to another. + payAddr := ref.PayAddr() + if payAddr != nil && *payAddr != BlankPayAddr { + if !bytes.Equal(invoice.PaymentAddr, payAddr[:]) { + return sqlc.Invoice{}, ErrInvRefEquivocation + } + } + + return invoice, nil } - // Otherwise the reference may include more fields, so we'll need to - // assemble the query parameters based on the fields that are set. - var params sqlc.GetInvoiceParams + // If the reference contains a payment address (AMP payments), look up + // directly by payment address using the unique index. + // + // NOTE: Pre-0.8 invoices do not have a payment address, and blank + // payment addresses are a special case for legacy keysend invoices. + // Those are handled by the hash fast path above. + payAddr := ref.PayAddr() + if payAddr != nil && *payAddr != BlankPayAddr { + invoice, err := db.GetInvoiceByAddr(ctx, payAddr[:]) + if errors.Is(err, sql.ErrNoRows) { + return sqlc.Invoice{}, ErrInvoiceNotFound + } + if err != nil { + return sqlc.Invoice{}, fmt.Errorf("unable to fetch "+ + "invoice by payment address: %w", err) + } - if ref.PayHash() != nil { - params.Hash = ref.PayHash()[:] + return invoice, nil } - // Newer invoices (0.11 and up) are indexed by payment address in - // addition to payment hash, but pre 0.8 invoices do not have one at - // all. Only allow lookups for payment address if it is not a blank - // payment address, which is a special-cased value for legacy keysend - // invoices. - if ref.PayAddr() != nil && *ref.PayAddr() != BlankPayAddr { - params.PaymentAddr = ref.PayAddr()[:] - } - - // If the reference has a set ID we'll fetch the invoice which has the - // corresponding AMP sub invoice. + // If only the set ID is given, look up via the AMP sub-invoice index. if ref.SetID() != nil { - params.SetID = ref.SetID()[:] + rows, err := db.GetInvoiceBySetID(ctx, ref.SetID()[:]) + if err != nil { + return sqlc.Invoice{}, fmt.Errorf("unable to fetch "+ + "invoice: %w", err) + } + + if len(rows) == 0 { + return sqlc.Invoice{}, ErrInvoiceNotFound + } + + if len(rows) > 1 { + return sqlc.Invoice{}, fmt.Errorf("ambiguous "+ + "invoice ref: set_id=%x matches %d invoices", + ref.SetID(), len(rows)) + } + + return rows[0], nil } - var ( - rows []sqlc.Invoice - err error - ) - - // We need to split the query based on how we intend to look up the - // invoice. If only the set ID is given then we want to have an exact - // match on the set ID. If other fields are given, we want to match on - // those fields and the set ID but with a less strict join condition. - if params.Hash == nil && params.PaymentAddr == nil && - params.SetID != nil { - - rows, err = db.GetInvoiceBySetID(ctx, params.SetID) - } else { - rows, err = db.GetInvoice(ctx, params) - } - - switch { - case len(rows) == 0: - return sqlc.Invoice{}, ErrInvoiceNotFound - - case len(rows) > 1: - // In case the reference is ambiguous, meaning it matches more - // than one invoice, we'll return an error. - return sqlc.Invoice{}, fmt.Errorf("ambiguous invoice ref: "+ - "%s: %s", ref.String(), lnutils.SpewLogClosure(rows)) - - case err != nil: - return sqlc.Invoice{}, fmt.Errorf("unable to fetch invoice: %w", - err) - } - - return rows[0], nil + return sqlc.Invoice{}, ErrInvoiceNotFound } // fetchInvoice fetches the common invoice data and the AMP state for the diff --git a/sqldb/sqlc/invoices.sql.go b/sqldb/sqlc/invoices.sql.go index 911c4e1d1..99e973924 100644 --- a/sqldb/sqlc/invoices.sql.go +++ b/sqldb/sqlc/invoices.sql.go @@ -476,6 +476,38 @@ func (q *Queries) GetInvoice(ctx context.Context, arg GetInvoiceParams) ([]Invoi return items, nil } +const getInvoiceByAddr = `-- name: GetInvoiceByAddr :one +SELECT i.id, i.hash, i.preimage, i.settle_index, i.settled_at, i.memo, i.amount_msat, i.cltv_delta, i.expiry, i.payment_addr, i.payment_request, i.payment_request_hash, i.state, i.amount_paid_msat, i.is_amp, i.is_hodl, i.is_keysend, i.created_at +FROM invoices i +WHERE i.payment_addr = $1 +` + +func (q *Queries) GetInvoiceByAddr(ctx context.Context, paymentAddr []byte) (Invoice, error) { + row := q.db.QueryRowContext(ctx, getInvoiceByAddr, paymentAddr) + var i Invoice + err := row.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ) + return i, err +} + const getInvoiceByHash = `-- name: GetInvoiceByHash :one SELECT i.id, i.hash, i.preimage, i.settle_index, i.settled_at, i.memo, i.amount_msat, i.cltv_delta, i.expiry, i.payment_addr, i.payment_request, i.payment_request_hash, i.state, i.amount_paid_msat, i.is_amp, i.is_hodl, i.is_keysend, i.created_at FROM invoices i diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 947237300..5b9c7d5ab 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -111,6 +111,7 @@ type Querier interface { // from different invoices. It is the caller's responsibility to ensure that // we bubble up an error in those cases. GetInvoice(ctx context.Context, arg GetInvoiceParams) ([]Invoice, error) + GetInvoiceByAddr(ctx context.Context, paymentAddr []byte) (Invoice, error) GetInvoiceByHash(ctx context.Context, hash []byte) (Invoice, error) GetInvoiceBySetID(ctx context.Context, setID []byte) ([]Invoice, error) GetInvoiceFeatures(ctx context.Context, invoiceID int64) ([]InvoiceFeature, error) diff --git a/sqldb/sqlc/queries/invoices.sql b/sqldb/sqlc/queries/invoices.sql index f9b92388d..22e5cd08e 100644 --- a/sqldb/sqlc/queries/invoices.sql +++ b/sqldb/sqlc/queries/invoices.sql @@ -59,6 +59,11 @@ SELECT i.* FROM invoices i WHERE i.hash = $1; +-- name: GetInvoiceByAddr :one +SELECT i.* +FROM invoices i +WHERE i.payment_addr = $1; + -- name: GetInvoiceBySetID :many SELECT i.* FROM invoices i