Skip to content

Commit e8213db

Browse files
authored
Merge pull request #9752 from erickcestari/reject-payment-without-invoice
routerrpc: reject payment to invoice that don't have payment secret or blinded paths
2 parents 6290edf + 831fefe commit e8213db

File tree

4 files changed

+56
-5
lines changed

4 files changed

+56
-5
lines changed

docs/release-notes/release-notes-0.20.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ circuit. The indices are only available for forwarding events saved after v0.20.
130130
`channel_update` message and handle it explicitly throughout the code base
131131
instead of extracting it from the TLV stream at various call-sites.
132132

133+
* [Require invoices to include a payment address or blinded paths](https://github.com/lightningnetwork/lnd/pull/9752)
134+
to comply with updated BOLT 11 specifications before sending payments.
135+
133136
## Testing
134137

135138
* Previously, automatic peer bootstrapping was disabled for simnet, signet and
@@ -152,6 +155,7 @@ circuit. The indices are only available for forwarding events saved after v0.20.
152155
* Abdulkbk
153156
* Boris Nagaev
154157
* Elle Mouton
158+
* Erick Cestari
155159
* Funyug
156160
* Mohamed Awnallah
157161
* Pins

lnrpc/routerrpc/router_backend.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/btcsuite/btcd/wire"
1616
sphinx "github.com/lightningnetwork/lightning-onion"
1717
"github.com/lightningnetwork/lnd/channeldb"
18+
"github.com/lightningnetwork/lnd/clock"
1819
"github.com/lightningnetwork/lnd/feature"
1920
"github.com/lightningnetwork/lnd/fn/v2"
2021
"github.com/lightningnetwork/lnd/htlcswitch"
@@ -118,6 +119,10 @@ type RouterBackend struct {
118119
// ShouldSetExpEndorsement returns a boolean indicating whether the
119120
// experimental endorsement bit should be set.
120121
ShouldSetExpEndorsement func() bool
122+
123+
// Clock is the clock used to validate payment requests expiry.
124+
// It is useful for testing.
125+
Clock clock.Clock
121126
}
122127

123128
// MissionControl defines the mission control dependencies of routerrpc.
@@ -980,11 +985,20 @@ func (r *RouterBackend) extractIntentFromSendRequest(
980985
}
981986

982987
// Next, we'll ensure that this payreq hasn't already expired.
983-
err = ValidatePayReqExpiry(payReq)
988+
err = ValidatePayReqExpiry(r.Clock, payReq)
984989
if err != nil {
985990
return nil, err
986991
}
987992

993+
// An invoice must include either a payment address or
994+
// blinded paths.
995+
if payReq.PaymentAddr.IsNone() &&
996+
len(payReq.BlindedPaymentPaths) == 0 {
997+
998+
return nil, errors.New("payment request must contain " +
999+
"either a payment address or blinded paths")
1000+
}
1001+
9881002
// If the amount was not included in the invoice, then we let
9891003
// the payer specify the amount of satoshis they wish to send.
9901004
// We override the amount to pay with the amount provided from
@@ -1001,7 +1015,7 @@ func (r *RouterBackend) extractIntentFromSendRequest(
10011015
if reqAmt != 0 {
10021016
return nil, errors.New("amount must not be " +
10031017
"specified when paying a non-zero " +
1004-
" amount invoice")
1018+
"amount invoice")
10051019
}
10061020

10071021
payIntent.Amount = *payReq.MilliSat
@@ -1370,10 +1384,10 @@ func UnmarshalFeatures(
13701384

13711385
// ValidatePayReqExpiry checks if the passed payment request has expired. In
13721386
// the case it has expired, an error will be returned.
1373-
func ValidatePayReqExpiry(payReq *zpay32.Invoice) error {
1387+
func ValidatePayReqExpiry(clock clock.Clock, payReq *zpay32.Invoice) error {
13741388
expiry := payReq.Expiry()
13751389
validUntil := payReq.Timestamp.Add(expiry)
1376-
if time.Now().After(validUntil) {
1390+
if clock.Now().After(validUntil) {
13771391
return fmt.Errorf("invoice expired. Valid until %v", validUntil)
13781392
}
13791393

lnrpc/routerrpc/router_backend_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"context"
66
"encoding/hex"
77
"testing"
8+
"time"
89

910
"github.com/btcsuite/btcd/btcutil"
1011
"github.com/btcsuite/btcd/chaincfg"
12+
"github.com/lightningnetwork/lnd/lnmock"
1113
"github.com/lightningnetwork/lnd/lnrpc"
1214
"github.com/lightningnetwork/lnd/lnwire"
1315
"github.com/lightningnetwork/lnd/record"
@@ -503,12 +505,22 @@ func TestExtractIntentFromSendRequest(t *testing.T) {
503505
"g6aykds4ydvf2x9lpngqcfux3hv8qlraan9v3s9296r5w5eh959yzadgh5ck" +
504506
"gjydgyfxdpumxtuk3p3caugmlqpz5necs"
505507

508+
const paymentReqMissingAddr = "lnbcrt100p1p70xwfzpp5qqqsyqcyq5rqwzqfq" +
509+
"qqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kge" +
510+
"tjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqnp4q0n326hr8v9zprg8" +
511+
"gsvezcch06gfaqqhde2aj730yg0durunfhv669qypqqqz3uu8wnr7883qzxr" +
512+
"566nuhled49fx6e6q0jn06w6gpgyznwzxwf8xdmye87kpx0y8lqtcgwywsau" +
513+
"0jkm66evelkw7cggwlegp4anv3cq62wusm"
514+
506515
destNodeBytes, err := hex.DecodeString(destKey)
507516
require.NoError(t, err)
508517

509518
target, err := route.NewVertexFromBytes(destNodeBytes)
510519
require.NoError(t, err)
511520

521+
mockClock := &lnmock.MockClock{}
522+
mockClock.On("Now").Return(time.Date(2025, 3, 1, 13, 0, 0, 0, time.UTC))
523+
512524
testCases := []extractIntentTestCase{
513525
{
514526
name: "Time preference out of range",
@@ -706,6 +718,7 @@ func TestExtractIntentFromSendRequest(t *testing.T) {
706718
return false
707719
},
708720
ActiveNetParams: &chaincfg.RegressionNetParams,
721+
Clock: mockClock,
709722
},
710723
sendReq: &SendPaymentRequest{
711724
Amt: int64(paymentAmount),
@@ -714,6 +727,23 @@ func TestExtractIntentFromSendRequest(t *testing.T) {
714727
valid: false,
715728
expectedErrorMsg: "invoice expired.",
716729
},
730+
{
731+
name: "Invoice missing payment address",
732+
backend: &RouterBackend{
733+
ShouldSetExpEndorsement: func() bool {
734+
return false
735+
},
736+
ActiveNetParams: &chaincfg.RegressionNetParams,
737+
MaxTotalTimelock: 1000,
738+
Clock: mockClock,
739+
},
740+
sendReq: &SendPaymentRequest{
741+
PaymentRequest: paymentReqMissingAddr,
742+
},
743+
valid: false,
744+
expectedErrorMsg: "payment request must contain " +
745+
"either a payment address or blinded paths",
746+
},
717747
{
718748
name: "Invalid dest vertex length",
719749
backend: &RouterBackend{

rpcserver.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server,
697697

698698
routerBackend := &routerrpc.RouterBackend{
699699
SelfNode: selfNode.PubKeyBytes,
700+
Clock: clock.NewDefaultClock(),
700701
FetchChannelCapacity: func(chanID uint64) (btcutil.Amount,
701702
error) {
702703

@@ -5616,7 +5617,9 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
56165617
}
56175618

56185619
// Next, we'll ensure that this payreq hasn't already expired.
5619-
err = routerrpc.ValidatePayReqExpiry(payReq)
5620+
err = routerrpc.ValidatePayReqExpiry(
5621+
r.routerBackend.Clock, payReq,
5622+
)
56205623
if err != nil {
56215624
return payIntent, err
56225625
}

0 commit comments

Comments
 (0)