Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 lnrpc/lightning.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3291,6 +3291,22 @@ message QueryRoutesRequest {
channel may be used.
*/
repeated uint64 outgoing_chan_ids = 20;

/*
An optional payment address to be included in the MPP record for the final
hop. This field is also referred to as the "payment secret" in BOLT 11.
Including this in the query allows the returned route to be used directly
with SendToRoute. This field is now required per the Lightning Network
specification and should always be provided for standard payments.
*/
bytes payment_addr = 21;

/*
An optional AMP record to use for AMP payments. If provided, this will be
included in the final hop of the route instead of an MPP record. The
payment_addr field should not be set if this is provided.
*/
AMPRecord amp_record = 22;
}

message NodePair {
Expand Down
159 changes: 159 additions & 0 deletions lnrpc/routerrpc/mpp_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package routerrpc

import (
"testing"

"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)

// TestUnmarshallRouteMPPValidation tests that UnmarshallRoute correctly
// enforces the MPP/AMP record validation on incoming routes.
func TestUnmarshallRouteMPPValidation(t *testing.T) {
t.Parallel()
backend := &RouterBackend{
SelfNode: route.Vertex{1, 2, 3},
}

// Test case 1: Route with no MPP or AMP record should succeed with
// a deprecation warning.
t.Run("no_mpp_or_amp", func(t *testing.T) {
rpcRoute := &lnrpc.Route{
TotalTimeLock: 100,
TotalAmtMsat: 1000,
Hops: []*lnrpc.Hop{
{
PubKey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
ChanId: 12345,
AmtToForwardMsat: 1000,
Expiry: 100,
},
},
}

// Should succeed with deprecation warning logged.
_, err := backend.UnmarshallRoute(rpcRoute)
require.NoError(t, err)
})

// Test case 2: Route with MPP record should succeed.
t.Run("with_mpp", func(t *testing.T) {
paymentAddr := make([]byte, 32)
paymentAddr[0] = 1

rpcRoute := &lnrpc.Route{
TotalTimeLock: 100,
TotalAmtMsat: 1000,
Hops: []*lnrpc.Hop{
{
PubKey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
ChanId: 12345,
AmtToForwardMsat: 1000,
Expiry: 100,
MppRecord: &lnrpc.MPPRecord{
PaymentAddr: paymentAddr,
TotalAmtMsat: 1000,
},
},
},
}

_, err := backend.UnmarshallRoute(rpcRoute)
require.NoError(t, err)
})

// Test case 3: Route with AMP record should succeed.
t.Run("with_amp", func(t *testing.T) {
rootShare := make([]byte, 32)
setID := make([]byte, 32)
rootShare[0] = 1
setID[0] = 2

rpcRoute := &lnrpc.Route{
TotalTimeLock: 100,
TotalAmtMsat: 1000,
Hops: []*lnrpc.Hop{
{
PubKey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
ChanId: 12345,
AmtToForwardMsat: 1000,
Expiry: 100,
AmpRecord: &lnrpc.AMPRecord{
RootShare: rootShare,
SetId: setID,
ChildIndex: 0,
},
},
},
}

_, err := backend.UnmarshallRoute(rpcRoute)
require.NoError(t, err)
})

// Test case 4: Route with both MPP and AMP records should fail.
t.Run("both_mpp_and_amp", func(t *testing.T) {
paymentAddr := make([]byte, 32)
rootShare := make([]byte, 32)
setID := make([]byte, 32)
paymentAddr[0] = 1
rootShare[0] = 1
setID[0] = 2

rpcRoute := &lnrpc.Route{
TotalTimeLock: 100,
TotalAmtMsat: 1000,
Hops: []*lnrpc.Hop{
{
PubKey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
ChanId: 12345,
AmtToForwardMsat: 1000,
Expiry: 100,
MppRecord: &lnrpc.MPPRecord{
PaymentAddr: paymentAddr,
TotalAmtMsat: 1000,
},
AmpRecord: &lnrpc.AMPRecord{
RootShare: rootShare,
SetId: setID,
ChildIndex: 0,
},
},
},
}

_, err := backend.UnmarshallRoute(rpcRoute)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot have both MPP and AMP")
})

// Test case 5: Blinded route should not require MPP/AMP.
t.Run("blinded_no_mpp", func(t *testing.T) {
blindingPoint := []byte{
0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb,
0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b,
0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28,
0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17,
0x98,
}

rpcRoute := &lnrpc.Route{
TotalTimeLock: 100,
TotalAmtMsat: 1000,
Hops: []*lnrpc.Hop{
{
PubKey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
ChanId: 12345,
AmtToForwardMsat: 1000,
Expiry: 100,
BlindingPoint: blindingPoint,
EncryptedData: []byte{1, 2, 3},
},
},
}

_, err := backend.UnmarshallRoute(rpcRoute)
require.NoError(t, err)
})
}
7 changes: 7 additions & 0 deletions lnrpc/routerrpc/router.proto
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,13 @@ message BuildRouteRequest {
*/
bytes payment_addr = 5;

/*
An optional AMP record to use for AMP payments. If provided, this will be
included in the final hop of the route instead of an MPP record. The
payment_addr field should not be set if this is provided.
*/
AMPRecord amp_record = 8;

/*
An optional field that can be used to pass an arbitrary set of TLV records
to the first hop peer of this payment. This can be used to pass application
Expand Down
77 changes: 76 additions & 1 deletion lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,54 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
return nil, err
}

// Parse payment_addr for MPP or amp_record for AMP payments.
// These are mutually exclusive and will be required for non-blinded
// payments as per Lightning Network specification.
// TODO(user): Once protos are regenerated with payment_addr and
// amp_record fields, uncomment the following code:
/*
var paymentAddr fn.Option[[32]byte]
var amp fn.Option[*record.AMP]

if len(in.PaymentAddr) > 0 {
var addr [32]byte
copy(addr[:], in.PaymentAddr)
paymentAddr = fn.Some(addr)
}

if in.AmpRecord != nil {
ampRec, err := UnmarshalAMP(in.AmpRecord)
if err != nil {
return nil, fmt.Errorf("invalid AMP record: %w", err)
}
amp = fn.Some(ampRec)
}

// Log warning if neither payment_addr nor amp_record provided
// for non-blinded payments.
if blindedPathSet == nil {
if paymentAddr.IsNone() && amp.IsNone() {
log.Warnf("QueryRoutes missing payment_addr or " +
"amp_record. This will be required in a " +
"future LND release as per Lightning " +
"Network specification.")
// TODO(v0.21.0): Uncomment to enforce validation.
// return nil, errors.New("payment_addr or amp_record " +
// "must be provided for standard payments as " +
// "required by Lightning Network specification")
}
}
*/

// Temporary: For now, use empty values until protos are regenerated.
// Remove this and uncomment above section after running 'make rpc'.
var paymentAddr fn.Option[[32]byte]
var amp fn.Option[*record.AMP]

return routing.NewRouteRequest(
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
customRecords, routeHintEdges, blindedPathSet,
finalCLTVDelta,
finalCLTVDelta, paymentAddr, amp,
)
}

Expand Down Expand Up @@ -826,6 +870,37 @@ func (r *RouterBackend) UnmarshallRoute(rpcroute *lnrpc.Route) (
prevNodePubKey = routeHop.PubKeyBytes
}

// Validate that the final hop contains either an MPP or AMP record.
// This will be required by the Lightning Network specification.
if len(hops) > 0 {
finalHop := hops[len(hops)-1]

// Check if blinded payment - blinded hops don't need MPP/AMP.
hasBlindingPoint := finalHop.BlindingPoint != nil

if !hasBlindingPoint {
// Final hop must have either MPP or AMP record.
if finalHop.MPP == nil && finalHop.AMP == nil {
log.Warnf("Route final hop missing MPP/AMP " +
"record. This will be required in a " +
"future LND release as per Lightning " +
"Network specification. Please update " +
"your client to include payment_addr or " +
"amp_record.")
// return nil, errors.New("final hop must include " +
// "either an MPP record (with payment_addr) or " +
// "an AMP record as required by the Lightning " +
// "Network specification")
}

// Cannot have both MPP and AMP records.
if finalHop.MPP != nil && finalHop.AMP != nil {
return nil, errors.New("final hop cannot have " +
"both MPP and AMP records")
}
}
}

route, err := route.NewRouteFromHops(
lnwire.MilliSatoshi(rpcroute.TotalAmtMsat),
rpcroute.TotalTimeLock,
Expand Down
64 changes: 64 additions & 0 deletions lnrpc/routerrpc/router_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
Expand Down Expand Up @@ -482,6 +483,7 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
ProbabilitySource: mc.GetProbability,
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
fn.None[[32]byte](), fn.None[*record.AMP](),
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1063,6 +1065,41 @@ func (s *Server) SendToRouteV2(ctx context.Context,
return nil, fmt.Errorf("unable to send, no routes provided")
}

// Perform early validation on the route before attempting to send.
// Check that the final hop has either an MPP or AMP record.
if len(req.Route.Hops) > 0 {
finalHop := req.Route.Hops[len(req.Route.Hops)-1]

// Check if this is a blinded payment (blinded hops don't need
// MPP/AMP).
hasBlindingPoint := len(finalHop.BlindingPoint) > 0

if !hasBlindingPoint {
hasMPP := finalHop.MppRecord != nil
hasAMP := finalHop.AmpRecord != nil

// Log warning if neither MPP nor AMP record present.
if !hasMPP && !hasAMP {
log.Warnf("Route final hop missing MPP/AMP " +
"record. This will be required in a " +
"future LND release as per Lightning " +
"Network specification. Please update " +
"your client to include payment_addr or " +
"amp_record.")
// return nil, errors.New("final hop must include " +
// "either an MPP record (with payment_addr) " +
// "or an AMP record as required by the " +
// "Lightning Network specification")
}

// Cannot have both.
if hasMPP && hasAMP {
return nil, errors.New("final hop cannot have " +
"both MPP and AMP records")
}
}
}

route, err := s.cfg.RouterBackend.UnmarshallRoute(req.Route)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1679,6 +1716,33 @@ func (s *Server) BuildRoute(_ context.Context,
payAddr = fn.Some(backingPayAddr)
}

// TODO(user): Once protos are regenerated with amp_record field,
// uncomment the following code to add AMP support:
/*
var amp fn.Option[*record.AMP]
if req.AmpRecord != nil {
ampRec, err := UnmarshalAMP(req.AmpRecord)
if err != nil {
return nil, fmt.Errorf("invalid AMP record: %w", err)
}
amp = fn.Some(ampRec)
}

// Validate that payment_addr and amp_record are mutually exclusive.
if payAddr.IsSome() && amp.IsSome() {
return nil, errors.New("payment_addr and amp_record " +
"cannot both be set")
}

// Require either payment_addr or amp_record to be set as per
// Lightning Network specification.
if payAddr.IsNone() && amp.IsNone() {
return nil, errors.New("either payment_addr (for MPP) or " +
"amp_record (for AMP) must be provided as required " +
"by Lightning Network specification")
}
*/

if req.FinalCltvDelta == 0 {
req.FinalCltvDelta = int32(
s.cfg.RouterBackend.DefaultFinalCltvDelta,
Expand Down
Loading