Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@

## RPC Additions

* `QueryRoutesRequest` now accepts a
[`payment_addr`](https://github.com/lightningnetwork/lnd/issues/9952)
field (the invoice payment secret). When set, an MPP record is injected
into the final hop of the returned route as required by the BOLT 11 spec.
An optional `amp_record` field is also added for AMP payments.

* `BuildRouteRequest` now accepts an
[`amp_record`](https://github.com/lightningnetwork/lnd/issues/9952)
field to allow building routes for AMP payments. `payment_addr` is now
required by `BuildRoute`.

* `SendToRouteV2` now
[enforces](https://github.com/lightningnetwork/lnd/issues/9952) that the
final hop of the provided route includes an MPP record, as `payment_secret`
is mandatory per the BOLT 11 spec.

## lncli Additions

# Improvements
Expand Down
528 changes: 276 additions & 252 deletions lnrpc/lightning.pb.go

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions lnrpc/lightning.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3360,6 +3360,19 @@ message QueryRoutesRequest {
channel may be used.
*/
repeated uint64 outgoing_chan_ids = 20;

/*
An optional payment address included in the invoice (also called payment
secret). If set, an MPP record containing this value will be included in
the final hop of the returned route.
*/
bytes payment_addr = 21;

/*
An optional AMP record to be included within the last hop of the route.
If set, payment_addr must also be set. See AMPRecord for details.
*/
AMPRecord amp_record = 22;
}

message NodePair {
Expand Down
38 changes: 38 additions & 0 deletions lnrpc/lightning.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,35 @@
"format": "uint64"
},
"collectionFormat": "multi"
},
{
"name": "payment_addr",
"description": "32 byte random value included in the invoice.\nThe receiver only accepts HTLSs that include this.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
},
{
"name": "amp_record.root_share",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
},
{
"name": "amp_record.set_id",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
},
{
"name": "amp_record.child_index",
"in": "query",
"required": false,
"type": "integer",
"format": "int64"
}
],
"tags": [
Expand Down Expand Up @@ -1692,6 +1721,15 @@
"format": "uint64"
},
"description": "The channel ids of the channels allowed for the first hop. If empty, any\nchannel may be used."
},
"payment_addr": {
"type": "string",
"format": "byte",
"description": "32 byte random value included in the invoice.\nThe receiver only accepts HTLSs that include this."
},
"amp_record": {
"$ref": "#/definitions/lnrpcAMPRecord",
"description": "An optional AMP record to be included within the last hop of the route.\nIf set, payment_addr must also be set. See AMPRecord for details."
}
}
}
Expand Down
184 changes: 99 additions & 85 deletions lnrpc/routerrpc/router.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lnrpc/routerrpc/router.proto
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,12 @@ message BuildRouteRequest {
base64.
*/
map<uint64, bytes> first_hop_custom_records = 6;

/*
An optional AMP record to be included within the last hop of the route.
If set, payment_addr must also be set. See AMPRecord for details.
*/
lnrpc.AMPRecord amp_record = 7;
}

message BuildRouteResponse {
Expand Down
4 changes: 4 additions & 0 deletions lnrpc/routerrpc/router.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,10 @@
"format": "byte"
},
"description": "An optional field that can be used to pass an arbitrary set of TLV records\nto the first hop peer of this payment. This can be used to pass application\nspecific data during the payment attempt. Record types are required to be in\nthe custom range \u003e= 65536. When using REST, the values must be encoded as\nbase64."
},
"amp_record": {
"$ref": "#/definitions/lnrpcAMPRecord",
"description": "An optional AMP record to be included within the last hop of the route.\nIf set, payment_addr must also be set. See AMPRecord for details."
}
}
},
Expand Down
21 changes: 21 additions & 0 deletions lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,27 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context,
return nil, err
}

// If payment_addr was provided inject MPP into final hop (BOLT 11)
if in.PaymentAddr != nil {
finalHop := route.FinalHop()
finalHop.MPP = record.NewMPP(finalHop.AmtToForward, [32]byte(in.PaymentAddr))
}
Comment thread
murraystewart96 marked this conversation as resolved.

// If AMP was provided inject into final hop alongside MPP record.
if in.AmpRecord != nil {
if len(in.PaymentAddr) == 0 {
return nil, errors.New("payment_addr must be set when " +
"amp_record is provided")
}

amp, err := UnmarshalAMP(in.AmpRecord)
if err != nil {
return nil, err
}

route.FinalHop().AMP = amp
}

// For each valid route, we'll convert the result into the format
// required by the RPC system.
rpcRoute, err := r.MarshallRoute(route)
Expand Down
40 changes: 31 additions & 9 deletions lnrpc/routerrpc/router_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
destKey = "0286098b97bc843372b4426d4b276cea9aa2f48f0428d6f5b66ae101befc14f8b4"
ignoreNodeKey = "02f274f48f3c0d590449a6776e3ce8825076ac376e470e992246eebc565ef8bb2a"
hintNodeKey = "0274e7fb33eafd74fe1acb6db7680bb4aa78e9c839a6e954e38abfad680f645ef7"
paymentAddr = "720eb5ee68523466ee822449296273de81eeab11093f3d5e20c50d6f557b97f4"

testMissionControlProb = 0.5
)
Expand All @@ -43,29 +44,29 @@ var (
// and passed onto path finding.
func TestQueryRoutes(t *testing.T) {
t.Run("no mission control", func(t *testing.T) {
testQueryRoutes(t, false, false, true, singleChanID)
testQueryRoutes(t, false, false, true, false, singleChanID)
})
t.Run("no mission control and msat", func(t *testing.T) {
testQueryRoutes(t, false, true, true, singleChanID)
t.Run("no mission control, using msat and MPP", func(t *testing.T) {
testQueryRoutes(t, false, true, true, true, singleChanID)
})
t.Run("with mission control", func(t *testing.T) {
testQueryRoutes(t, true, false, true, singleChanID)
testQueryRoutes(t, true, false, true, false, singleChanID)
})
t.Run("no mission control bad cltv limit", func(t *testing.T) {
testQueryRoutes(t, false, false, false, singleChanID)
testQueryRoutes(t, false, false, false, false, singleChanID)
})

t.Run("both outgoing chan id and chan ids", func(t *testing.T) {
testQueryRoutes(t, true, false, true, bothChanIds)
testQueryRoutes(t, true, false, true, false, bothChanIds)
})

t.Run("multiple outgoing chan ids", func(t *testing.T) {
testQueryRoutes(t, false, true, true, multiChanID)
testQueryRoutes(t, false, true, true, false, multiChanID)
})
}

func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
setTimelock bool, outgoingChanConfig string) {
func testQueryRoutes(t *testing.T, useMissionControl, useMsat,
setTimelock, useMPP bool, outgoingChanConfig string) {

ignoreNodeBytes, err := hex.DecodeString(ignoreNodeKey)
if err != nil {
Expand Down Expand Up @@ -102,6 +103,14 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
},
}

var paymentAddrBytes []byte
if useMPP {
paymentAddrBytes, err = hex.DecodeString(paymentAddr)
if err != nil {
t.Fatal(err)
}
}

request := &lnrpc.QueryRoutesRequest{
PubKey: destKey,
FinalCltvDelta: 100,
Expand All @@ -118,6 +127,7 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
LastHopPubkey: lastHop[:],
DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_MPP_OPT},
RouteHints: rpcRouteHints,
PaymentAddr: paymentAddrBytes,
}

amtSat := int64(100000)
Expand Down Expand Up @@ -224,6 +234,10 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
}

hops := []*route.Hop{{}}
if useMsat {
hops = []*route.Hop{{AmtToForward: lnwire.MilliSatoshi(amtSat * 1000)}}
}

route, err := route.NewRouteFromHops(
req.Amount, 144, req.Source, hops,
)
Expand Down Expand Up @@ -285,6 +299,14 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
if len(resp.Routes) != 1 {
t.Fatal("expected a single route response")
}

// If we are using MPP (Bolt 11) then we should expect the last hop to have one set
if useMPP {
finalHop := resp.Routes[0].Hops[len(resp.Routes[0].Hops)-1]
require.NotNil(t, finalHop.MppRecord)
require.Equal(t, request.PaymentAddr, finalHop.MppRecord.PaymentAddr)
require.Equal(t, amtSat*1000, finalHop.MppRecord.TotalAmtMsat)
}
}

type mockMissionControl struct {
Expand Down
27 changes: 22 additions & 5 deletions lnrpc/routerrpc/router_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,10 @@ func (s *Server) SendToRouteV2(ctx context.Context,
return nil, err
}

if route.FinalHop().MPP == nil {
return nil, fmt.Errorf("unable to send, no MPP provided")
}

hash, err := lntypes.MakeHash(req.PaymentHash)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1675,12 +1679,25 @@ func (s *Server) BuildRoute(_ context.Context,
outgoingChan = &req.OutgoingChanId
}

// Enforce payment_addr
if len(req.PaymentAddr) == 0 {
return nil, errors.New("payment_addr must be set")
}
Comment thread
murraystewart96 marked this conversation as resolved.
Outdated

var payAddr fn.Option[[32]byte]
if len(req.PaymentAddr) != 0 {
var backingPayAddr [32]byte
copy(backingPayAddr[:], req.PaymentAddr)
var backingPayAddr [32]byte
copy(backingPayAddr[:], req.PaymentAddr)
payAddr = fn.Some(backingPayAddr)

payAddr = fn.Some(backingPayAddr)
// Optional AMP record
if req.AmpRecord != nil {
return nil, errors.New("payment_addr must be set when " +
"amp_record is provided")
}

ampRecord, err := UnmarshalAMP(req.AmpRecord)
if err != nil {
return nil, err
}
Comment thread
murraystewart96 marked this conversation as resolved.

if req.FinalCltvDelta == 0 {
Expand Down Expand Up @@ -1708,7 +1725,7 @@ func (s *Server) BuildRoute(_ context.Context,
// Build the route and return it to the caller.
route, err := s.cfg.Router.BuildRoute(
amt, hops, outgoingChan, req.FinalCltvDelta, payAddr,
firstHopBlob,
ampRecord, firstHopBlob,
)
if err != nil {
return nil, err
Expand Down
8 changes: 8 additions & 0 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type finalHopParams struct {

records record.CustomSet
paymentAddr fn.Option[[32]byte]
amp *record.AMP

// metadata is additional data that is sent along with the payment to
// the payee.
Expand Down Expand Up @@ -265,6 +266,12 @@ func newRoute(sourceVertex route.Vertex,
mpp = record.NewMPP(finalHop.totalAmt, addr)
})

// If we're attaching an AMP record but the receiver doesn't support
// AMP, fail.
if finalHop.amp != nil && !supports(lnwire.AMPOptional) {
return nil, errors.New("cannot attach AMP record")
}

metadata = finalHop.metadata

if blindedPathSet != nil {
Expand Down Expand Up @@ -312,6 +319,7 @@ func newRoute(sourceVertex route.Vertex,
OutgoingTimeLock: outgoingTimeLock,
CustomRecords: customRecords,
MPP: mpp,
AMP: finalHop.amp,
Metadata: metadata,
TotalAmtMsat: totalAmtMsatBlinded,
}
Expand Down
Loading
Loading