diff --git a/lnrpc/offersrpc/offers_server.go b/lnrpc/offersrpc/offers_server.go index 9d51a179059..ce9e5f8cb07 100644 --- a/lnrpc/offersrpc/offers_server.go +++ b/lnrpc/offersrpc/offers_server.go @@ -674,7 +674,7 @@ func (s *Server) PayOffer(ctx context.Context, // The invoice paid is the payment's request: kept with the payment, // it is, with the preimage, the proof of what was paid and to whom // (the issuer signed it), as a BOLT11 request is for those payments. - intent.PaymentRequest = []byte(bolt12) + intent.PaymentRequest = []byte(storedInvoice(bolt12)) hash := intent.Identifier() preimage, rt, err := s.cfg.Deps.PayInvoice(ctx, intent) @@ -815,3 +815,26 @@ func invoiceToRPC(inv *bolt12.Invoice) *InvoiceInfo { SignatureValid: bolt12.VerifyInvoice(inv) == nil, } } + +// MaxStoredInvoice bounds the BOLT 12 invoice kept as a payment's request. +// A BOLT 11 request is at most about 7 KB; a BOLT 12 invoice its issuer pads +// (unknown odd TLVs) could run to megabytes, and every listpayments page +// holding it would carry them, past what a gRPC client accepts by default. +const MaxStoredInvoice = 32 * 1024 + +// storedInvoice is the invoice to keep with a payment: re-encoded from its +// bytes (no '+' continuations or whitespace, the same signed TLVs, so it is +// still the issuer's signature over what was paid), or nothing when that is +// above MaxStoredInvoice or it does not decode. +func storedInvoice(s string) string { + hrp, data, err := bolt12.Decode(strings.TrimSpace(s)) + if err != nil { + return "" + } + enc, err := bolt12.Encode(hrp, data) + if err != nil || len(enc) > MaxStoredInvoice { + return "" + } + + return enc +} diff --git a/lnrpc/offersrpc/offers_server_test.go b/lnrpc/offersrpc/offers_server_test.go index 4a80dfafdd1..cdc966d0421 100644 --- a/lnrpc/offersrpc/offers_server_test.go +++ b/lnrpc/offersrpc/offers_server_test.go @@ -4,6 +4,7 @@ package offersrpc import ( + "bytes" "context" "errors" "strings" @@ -804,3 +805,22 @@ func fetchPayAndList(t *testing.T, chain [32]byte) { }) requireCode(t, err, codes.InvalidArgument) } + +// TestStoredInvoice checks the invoice kept with a payment: the same bytes, +// re-encoded without continuations, and nothing above MaxStoredInvoice. +func TestStoredInvoice(t *testing.T) { + data := bytes.Repeat([]byte{0x42}, 600) + plain, err := bolt12.Encode(offers.InvoiceHRP, data) + require.NoError(t, err) + + padded := plain[:20] + "+\n " + plain[20:300] + "+ " + plain[300:] + require.Equal(t, plain, storedInvoice(" "+padded+"\n")) + + big, err := bolt12.Encode( + offers.InvoiceHRP, make([]byte, MaxStoredInvoice), + ) + require.NoError(t, err) + require.Empty(t, storedInvoice(big)) + + require.Empty(t, storedInvoice("not an invoice")) +}