Skip to content

Commit d41b86b

Browse files
authored
Merge pull request #7852 from ziggie1984/pathfind_sizeestimate
Path Finding Size Estimation including Blinded Path Data
2 parents 54c47a3 + da5055c commit d41b86b

19 files changed

+847
-117
lines changed

docs/release-notes/release-notes-0.18.0.md

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777

7878
* [Add Taproot witness types
7979
to rpc](https://github.com/lightningnetwork/lnd/pull/8431)
80+
81+
* [Fixed](https://github.com/lightningnetwork/lnd/pull/7852) the payload size
82+
calculation in our pathfinder because blinded hops introduced new tlv records.
8083

8184
# New Features
8285
## Functional Enhancements

itest/lnd_amp_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ func testSendPaymentAMPInvoiceCase(ht *lntest.HarnessTest,
104104
// expect an extra invoice to appear in the ListInvoices response, since
105105
// a new invoice will be JIT inserted under a different payment address
106106
// than the one in the invoice.
107+
//
108+
// NOTE: This will only work when the peer has spontaneous AMP payments
109+
// enabled otherwise no invoice under a different payment_addr will be
110+
// found.
107111
var (
108112
expNumInvoices = 1
109113
externalPayAddr []byte

lnrpc/routerrpc/router_backend.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ 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/channeldb/models"
1918
"github.com/lightningnetwork/lnd/feature"
2019
"github.com/lightningnetwork/lnd/htlcswitch"
2120
"github.com/lightningnetwork/lnd/lnrpc"
@@ -280,7 +279,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
280279
// inside of the path rather than the request's fields.
281280
var (
282281
targetPubKey *route.Vertex
283-
routeHintEdges map[route.Vertex][]*models.CachedEdgePolicy
282+
routeHintEdges map[route.Vertex][]routing.AdditionalEdge
284283
blindedPmt *routing.BlindedPayment
285284

286285
// finalCLTVDelta varies depending on whether we're sending to
@@ -391,6 +390,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
391390
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
392391
CltvLimit: cltvLimit,
393392
DestFeatures: destinationFeatures,
393+
BlindedPayment: blindedPmt,
394394
}
395395

396396
// Pass along an outgoing channel restriction if specified.
@@ -967,6 +967,9 @@ func (r *RouterBackend) extractIntentFromSendRequest(
967967
// pseudo-reusable, e.g. the invoice parameters are
968968
// reused (amt, cltv, hop hints, etc) even though the
969969
// payments will share different payment hashes.
970+
//
971+
// NOTE: This will only work when the peer has
972+
// spontaneous AMP payments enabled.
970973
if len(rpcPayReq.PaymentAddr) > 0 {
971974
var addr [32]byte
972975
copy(addr[:], rpcPayReq.PaymentAddr)

record/amp.go

+10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ type AMP struct {
1919
childIndex uint32
2020
}
2121

22+
// MaxAmpPayLoadSize is an AMP Record which when serialized to a tlv record uses
23+
// the maximum payload size. The `childIndex` is created randomly and is a
24+
// 4 byte `varint` type so we make sure we use an index which will be encoded in
25+
// 4 bytes.
26+
var MaxAmpPayLoadSize = AMP{
27+
rootShare: [32]byte{},
28+
setID: [32]byte{},
29+
childIndex: 0x80000000,
30+
}
31+
2232
// NewAMP generate a new AMP record with the given root_share, set_id, and
2333
// child_index.
2434
func NewAMP(rootShare, setID [32]byte, childIndex uint32) *AMP {

routing/additional_edge.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package routing
2+
3+
import (
4+
"errors"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/lightningnetwork/lnd/channeldb/models"
8+
"github.com/lightningnetwork/lnd/lnwire"
9+
"github.com/lightningnetwork/lnd/routing/route"
10+
)
11+
12+
var (
13+
// ErrNoPayLoadSizeFunc is returned when no payload size function is
14+
// definied.
15+
ErrNoPayLoadSizeFunc = errors.New("no payloadSizeFunc defined for " +
16+
"additional edge")
17+
)
18+
19+
// AdditionalEdge is an interface which specifies additional edges which can
20+
// be appended to an existing route. Compared to normal edges of a route they
21+
// provide an explicit payload size function and are introduced because blinded
22+
// paths differ in their payload structure.
23+
type AdditionalEdge interface {
24+
// IntermediatePayloadSize returns the size of the payload for the
25+
// additional edge when being an intermediate hop in a route NOT the
26+
// final hop.
27+
IntermediatePayloadSize(amount lnwire.MilliSatoshi, expiry uint32,
28+
legacy bool, channelID uint64) uint64
29+
30+
// EdgePolicy returns the policy of the additional edge.
31+
EdgePolicy() *models.CachedEdgePolicy
32+
}
33+
34+
// PayloadSizeFunc defines the interface for the payload size function.
35+
type PayloadSizeFunc func(amount lnwire.MilliSatoshi, expiry uint32,
36+
legacy bool, channelID uint64) uint64
37+
38+
// PrivateEdge implements the AdditionalEdge interface. As the name implies it
39+
// is used for private route hints that the receiver adds for example to an
40+
// invoice.
41+
type PrivateEdge struct {
42+
policy *models.CachedEdgePolicy
43+
}
44+
45+
// EdgePolicy return the policy of the PrivateEdge.
46+
func (p *PrivateEdge) EdgePolicy() *models.CachedEdgePolicy {
47+
return p.policy
48+
}
49+
50+
// IntermediatePayloadSize returns the sphinx payload size defined in BOLT04 if
51+
// this edge were to be included in a route.
52+
func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi,
53+
expiry uint32, legacy bool, channelID uint64) uint64 {
54+
55+
hop := route.Hop{
56+
AmtToForward: amount,
57+
OutgoingTimeLock: expiry,
58+
LegacyPayload: legacy,
59+
}
60+
61+
return hop.PayloadSize(channelID)
62+
}
63+
64+
// BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed
65+
// as additional edges because they are appened at the end of a normal route.
66+
type BlindedEdge struct {
67+
policy *models.CachedEdgePolicy
68+
cipherText []byte
69+
blindingPoint *btcec.PublicKey
70+
}
71+
72+
// EdgePolicy return the policy of the BlindedEdge.
73+
func (b *BlindedEdge) EdgePolicy() *models.CachedEdgePolicy {
74+
return b.policy
75+
}
76+
77+
// IntermediatePayloadSize returns the sphinx payload size defined in BOLT04 if
78+
// this edge were to be included in a route.
79+
func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32,
80+
_ bool, _ uint64) uint64 {
81+
82+
hop := route.Hop{
83+
BlindingPoint: b.blindingPoint,
84+
LegacyPayload: false,
85+
EncryptedData: b.cipherText,
86+
}
87+
88+
// For blinded paths the next chanID is in the encrypted data tlv.
89+
return hop.PayloadSize(0)
90+
}
91+
92+
// Compile-time constraints to ensure the PrivateEdge and the BlindedEdge
93+
// implement the AdditionalEdge interface.
94+
var _ AdditionalEdge = (*PrivateEdge)(nil)
95+
var _ AdditionalEdge = (*BlindedEdge)(nil)
96+
97+
// defaultHopPayloadSize is the default payload size of a normal (not-blinded)
98+
// hop in the route.
99+
func defaultHopPayloadSize(amount lnwire.MilliSatoshi, expiry uint32,
100+
legacy bool, channelID uint64) uint64 {
101+
102+
// The payload size of a cleartext intermediate hop is equal to the
103+
// payload size of a private edge therefore we reuse its size function.
104+
edge := PrivateEdge{}
105+
106+
return edge.IntermediatePayloadSize(amount, expiry, legacy, channelID)
107+
}

routing/additional_edge_test.go

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package routing
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"encoding/hex"
7+
"testing"
8+
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
sphinx "github.com/lightningnetwork/lightning-onion"
11+
"github.com/lightningnetwork/lnd/routing/route"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// TestIntermediatePayloadSize tests the payload size functions of the
16+
// PrivateEdge and the BlindedEdge.
17+
func TestIntermediatePayloadSize(t *testing.T) {
18+
t.Parallel()
19+
20+
testPrivKeyBytes, _ := hex.DecodeString("e126f68f7eafcc8b74f54d269fe" +
21+
"206be715000f94dac067d1c04a8ca3b2db734")
22+
_, blindedPoint := btcec.PrivKeyFromBytes(testPrivKeyBytes)
23+
24+
testCases := []struct {
25+
name string
26+
hop route.Hop
27+
nextHop uint64
28+
edge AdditionalEdge
29+
}{
30+
{
31+
name: "Legacy payload private edge",
32+
hop: route.Hop{
33+
AmtToForward: 1000,
34+
OutgoingTimeLock: 600000,
35+
ChannelID: 3432483437438,
36+
LegacyPayload: true,
37+
},
38+
nextHop: 1,
39+
edge: &PrivateEdge{},
40+
},
41+
{
42+
name: "Tlv payload private edge",
43+
hop: route.Hop{
44+
AmtToForward: 1000,
45+
OutgoingTimeLock: 600000,
46+
ChannelID: 3432483437438,
47+
LegacyPayload: false,
48+
},
49+
nextHop: 1,
50+
edge: &PrivateEdge{},
51+
},
52+
{
53+
name: "Blinded edge",
54+
hop: route.Hop{
55+
EncryptedData: []byte{12, 13},
56+
},
57+
edge: &BlindedEdge{
58+
cipherText: []byte{12, 13},
59+
},
60+
},
61+
{
62+
name: "Blinded edge - introduction point",
63+
hop: route.Hop{
64+
EncryptedData: []byte{12, 13},
65+
BlindingPoint: blindedPoint,
66+
},
67+
edge: &BlindedEdge{
68+
cipherText: []byte{12, 13},
69+
blindingPoint: blindedPoint,
70+
},
71+
},
72+
}
73+
74+
for _, testCase := range testCases {
75+
testCase := testCase
76+
77+
t.Run(testCase.name, func(t *testing.T) {
78+
t.Parallel()
79+
80+
payLoad, err := createHopPayload(
81+
testCase.hop, testCase.nextHop, false,
82+
)
83+
require.NoErrorf(t, err, "failed to create hop payload")
84+
85+
expectedPayloadSize := testCase.edge.
86+
IntermediatePayloadSize(
87+
testCase.hop.AmtToForward,
88+
testCase.hop.OutgoingTimeLock,
89+
testCase.hop.LegacyPayload,
90+
testCase.nextHop,
91+
)
92+
93+
require.Equal(
94+
t, expectedPayloadSize,
95+
uint64(payLoad.NumBytes()),
96+
)
97+
})
98+
}
99+
}
100+
101+
// createHopPayload creates the hop payload of the sphinx package to facilitate
102+
// the testing of the payload size.
103+
func createHopPayload(hop route.Hop, nextHop uint64,
104+
finalHop bool) (sphinx.HopPayload, error) {
105+
106+
// If this is the legacy payload, then we can just include the
107+
// hop data as normal.
108+
if hop.LegacyPayload {
109+
// Before we encode this value, we'll pack the next hop
110+
// into the NextAddress field of the hop info to ensure
111+
// we point to the right now.
112+
hopData := sphinx.HopData{
113+
ForwardAmount: uint64(hop.AmtToForward),
114+
OutgoingCltv: hop.OutgoingTimeLock,
115+
}
116+
binary.BigEndian.PutUint64(
117+
hopData.NextAddress[:], nextHop,
118+
)
119+
120+
return sphinx.NewLegacyHopPayload(&hopData)
121+
}
122+
123+
// For non-legacy payloads, we'll need to pack the
124+
// routing information, along with any extra TLV
125+
// information into the new per-hop payload format.
126+
// We'll also pass in the chan ID of the hop this
127+
// channel should be forwarded to so we can construct a
128+
// valid payload.
129+
var b bytes.Buffer
130+
err := hop.PackHopPayload(&b, nextHop, finalHop)
131+
if err != nil {
132+
return sphinx.HopPayload{}, err
133+
}
134+
135+
return sphinx.NewTLVHopPayload(b.Bytes())
136+
}

0 commit comments

Comments
 (0)