Skip to content

Commit 90befa4

Browse files
committed
itest: add failing test for remote graph policy update
Add testRemoteGraphPolicyUpdate which demonstrates that when a payment fails due to a stale fee policy (FeeInsufficient), the updated ChannelUpdate from the failure message is NOT applied to the sender's graph cache when the channel only exists in the remote graph source. The test asserts the current broken behavior: the payment fails with FAILURE_REASON_NO_ROUTE because Zane cannot learn the updated fee from the failure message, and the cached policy remains stale. Also includes a small fix to DBGraphSource.SourceNode (use ErrSourceNodeNotSet instead of ErrGraphNodeNotFound) and a TODO for making remote cache population async once #10065 lands.
1 parent 8d5575e commit 90befa4

4 files changed

Lines changed: 263 additions & 1 deletion

File tree

graph/db/graph_source.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ func (d *DBGraphSource) SourceNode(ctx context.Context) (*models.Node,
344344
func(v lnwire.GossipVersion) (*models.Node, error) {
345345
return d.cg.db.SourceNode(ctx, v)
346346
},
347-
ErrGraphNodeNotFound,
347+
ErrSourceNodeNotSet,
348348
)
349349
}
350350

graph/db/graph_source_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package graphdb
2+
3+
import (
4+
"testing"
5+
6+
"github.com/lightningnetwork/lnd/lnwire"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestDBGraphSourceSourceNodeFallback ensures the graph source falls back to
11+
// the next supported gossip version when the preferred version does not yet
12+
// have a source node persisted.
13+
func TestDBGraphSourceSourceNodeFallback(t *testing.T) {
14+
t.Parallel()
15+
16+
ctx := t.Context()
17+
graph := MakeTestGraph(t)
18+
source := NewDBGraphSource(graph)
19+
20+
_, err := source.SourceNode(ctx)
21+
require.ErrorIs(t, err, ErrSourceNodeNotSet)
22+
23+
node := createTestVertex(t, lnwire.GossipVersion1)
24+
require.NoError(t, graph.SetSourceNode(ctx, node))
25+
26+
sourceNode, err := source.SourceNode(ctx)
27+
require.NoError(t, err)
28+
compareNodes(t, node, sourceNode)
29+
}

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,10 @@ var allTestCases = []*lntest.TestCase{
783783
Name: "remote graph",
784784
TestFunc: testRemoteGraph,
785785
},
786+
{
787+
Name: "remote graph policy update",
788+
TestFunc: testRemoteGraphPolicyUpdate,
789+
},
786790
}
787791

788792
// appendPrefixed is used to add a prefix to each test name in the subtests

itest/lnd_remote_graph_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/btcsuite/btcd/btcutil"
1111
"github.com/lightningnetwork/lnd/lnrpc"
12+
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1213
"github.com/lightningnetwork/lnd/lntest"
1314
"github.com/lightningnetwork/lnd/lntest/node"
1415
"github.com/lightningnetwork/lnd/lntest/wait"
@@ -201,3 +202,231 @@ func testRemoteGraph(ht *lntest.HarnessTest) {
201202
}, wait.DefaultTimeout)
202203
require.NoError(ht.T, err)
203204
}
205+
206+
// testRemoteGraphPolicyUpdate tests that when a payment fails because of a
207+
// stale fee policy (FeeInsufficient), the updated ChannelUpdate from the
208+
// failure message is applied to the sender's graph cache — even when the
209+
// channel only exists in the remote graph and not in the sender's local DB.
210+
func testRemoteGraphPolicyUpdate(ht *lntest.HarnessTest) {
211+
var (
212+
ctx = context.Background()
213+
descGraphReq = &lnrpc.ChannelGraphRequest{
214+
IncludeUnannounced: true,
215+
}
216+
)
217+
218+
// Set up a network:
219+
// Alice <- Bob <- Carol
220+
_, nodes := ht.CreateSimpleNetwork(
221+
[][]string{nil, nil, nil}, lntest.OpenChannelParams{
222+
Amt: btcutil.Amount(100000),
223+
},
224+
)
225+
carol, bob, alice := nodes[0], nodes[1], nodes[2]
226+
227+
// Create graph provider node, Greg. Connect it to Bob so it syncs the
228+
// public graph.
229+
greg := ht.NewNode("Greg", nil)
230+
ht.EnsureConnected(greg, bob)
231+
232+
// Wait until Greg has synced the public graph.
233+
err := wait.NoError(func() error {
234+
resp := greg.RPC.DescribeGraph(descGraphReq)
235+
if len(resp.Edges) != 2 {
236+
return fmt.Errorf("greg has %d edges, want 2",
237+
len(resp.Edges))
238+
}
239+
240+
return nil
241+
}, wait.DefaultTimeout)
242+
require.NoError(ht.T, err)
243+
244+
// Create Zane, using Greg as its remote graph source with gossip
245+
// sync disabled.
246+
zane := ht.NewNode("Zane", []string{
247+
"--gossip.no-sync",
248+
"--remotegraph.enable",
249+
"--caches.rpc-graph-cache-duration=0",
250+
fmt.Sprintf(
251+
"--remotegraph.rpchost=localhost:%d",
252+
greg.Cfg.RPCPort,
253+
),
254+
fmt.Sprintf(
255+
"--remotegraph.tlscertpath=%s",
256+
greg.Cfg.TLSCertPath,
257+
),
258+
fmt.Sprintf(
259+
"--remotegraph.macaroonpath=%s",
260+
greg.Cfg.AdminMacPath,
261+
),
262+
})
263+
264+
// Fund Zane, connect to Carol, and open a private channel.
265+
ht.FundCoins(btcutil.SatoshiPerBitcoin, zane)
266+
ht.EnsureConnected(zane, carol)
267+
chanPointZane := ht.OpenChannel(
268+
zane, carol, lntest.OpenChannelParams{
269+
Private: true,
270+
Amt: btcutil.Amount(100000),
271+
},
272+
)
273+
274+
// Wait until Zane sees the full graph (2 public + 1 private = 3
275+
// edges).
276+
err = wait.NoError(func() error {
277+
resp := zane.RPC.DescribeGraph(descGraphReq)
278+
if len(resp.Edges) != 3 {
279+
return fmt.Errorf("zane has %d edges, want 3",
280+
len(resp.Edges))
281+
}
282+
283+
return nil
284+
}, wait.DefaultTimeout)
285+
require.NoError(ht.T, err)
286+
287+
// Verify Zane can route a payment through the remote graph.
288+
invoice := alice.RPC.AddInvoice(&lnrpc.Invoice{Value: 100})
289+
ht.CompletePaymentRequests(zane, []string{invoice.PaymentRequest})
290+
291+
// Record the Bob->Alice channel ID so we can query its policy later.
292+
bobGraph := bob.RPC.DescribeGraph(descGraphReq)
293+
var bobAliceChanID uint64
294+
for _, edge := range bobGraph.Edges {
295+
isBA := edge.Node1Pub == bob.PubKeyStr &&
296+
edge.Node2Pub == alice.PubKeyStr
297+
isAB := edge.Node1Pub == alice.PubKeyStr &&
298+
edge.Node2Pub == bob.PubKeyStr
299+
300+
if isBA || isAB {
301+
bobAliceChanID = edge.ChannelId
302+
303+
break
304+
}
305+
}
306+
require.NotZero(ht.T, bobAliceChanID,
307+
"bob-alice channel not found")
308+
309+
// Disconnect Greg from Bob so Greg won't receive the upcoming fee
310+
// update through gossip. This means Zane's remote graph subscription
311+
// won't deliver the update either.
312+
ht.DisconnectNodes(greg, bob)
313+
314+
// Query Zane's current view of the Bob->Alice channel's fee policy
315+
// so we know the "old" value.
316+
zaneChanInfo, err := zane.RPC.LN.GetChanInfo(
317+
ctx, &lnrpc.ChanInfoRequest{ChanId: bobAliceChanID},
318+
)
319+
require.NoError(ht.T, err)
320+
321+
// Determine which policy is Bob's. Bob is the node that charges fees
322+
// for forwarding on this channel.
323+
var oldBobFeeRate int64
324+
if zaneChanInfo.Node1Pub == bob.PubKeyStr {
325+
oldBobFeeRate = zaneChanInfo.Node1Policy.FeeRateMilliMsat
326+
} else {
327+
oldBobFeeRate = zaneChanInfo.Node2Policy.FeeRateMilliMsat
328+
}
329+
330+
// Bob dramatically increases his fee on the Bob->Alice channel.
331+
newFeeRate := oldBobFeeRate + 500_000
332+
bob.RPC.UpdateChannelPolicy(&lnrpc.PolicyUpdateRequest{
333+
Scope: &lnrpc.PolicyUpdateRequest_Global{
334+
Global: true,
335+
},
336+
BaseFeeMsat: 1000,
337+
FeeRate: float64(newFeeRate) / 1_000_000,
338+
TimeLockDelta: 80,
339+
})
340+
341+
// Give Alice a moment to receive the update from Bob (they're
342+
// connected), but Greg is disconnected so neither Greg nor Zane
343+
// will learn of the update through gossip/subscription.
344+
err = wait.NoError(func() error {
345+
info, err := alice.RPC.LN.GetChanInfo(
346+
ctx,
347+
&lnrpc.ChanInfoRequest{ChanId: bobAliceChanID},
348+
)
349+
if err != nil {
350+
return err
351+
}
352+
353+
var feeRate int64
354+
if info.Node1Pub == bob.PubKeyStr {
355+
feeRate = info.Node1Policy.FeeRateMilliMsat
356+
} else {
357+
feeRate = info.Node2Policy.FeeRateMilliMsat
358+
}
359+
if feeRate != newFeeRate {
360+
return fmt.Errorf("alice sees fee rate %d, "+
361+
"want %d", feeRate, newFeeRate)
362+
}
363+
364+
return nil
365+
}, wait.DefaultTimeout)
366+
require.NoError(ht.T, err)
367+
368+
// Zane should still have the OLD fee policy for Bob since Greg is
369+
// disconnected and can't forward the update.
370+
zaneChanInfo, err = zane.RPC.LN.GetChanInfo(
371+
ctx, &lnrpc.ChanInfoRequest{ChanId: bobAliceChanID},
372+
)
373+
require.NoError(ht.T, err)
374+
375+
var zaneBobFeeRate int64
376+
if zaneChanInfo.Node1Pub == bob.PubKeyStr {
377+
zaneBobFeeRate = zaneChanInfo.Node1Policy.FeeRateMilliMsat
378+
} else {
379+
zaneBobFeeRate = zaneChanInfo.Node2Policy.FeeRateMilliMsat
380+
}
381+
require.Equal(ht.T, oldBobFeeRate, zaneBobFeeRate,
382+
"zane should still see old fee rate")
383+
384+
// Now Zane tries to pay Alice again. The payment will be attempted
385+
// with the stale fee policy. Bob will reject it with FeeInsufficient
386+
// and include the updated ChannelUpdate in the failure message.
387+
//
388+
// The router will extract the ChannelUpdate and call
389+
// ApplyChannelUpdate. Currently, this fails silently because the
390+
// channel only exists in the remote graph (via Greg), not in Zane's
391+
// local DB.
392+
//
393+
// We use a high fee limit to ensure the payment would succeed if
394+
// Zane had the correct fee info.
395+
invoice2 := alice.RPC.AddInvoice(&lnrpc.Invoice{Value: 100})
396+
397+
// TODO(elle): Once the fix is applied, this payment should succeed
398+
// because ApplyChannelUpdate will update the graph cache from the
399+
// failure message, and the router will retry with the correct fees.
400+
// For now, we assert the BROKEN behavior: the payment fails because
401+
// Zane can't learn the updated fee from the failure message.
402+
ht.SendPaymentAssertFail(
403+
zane,
404+
&routerrpc.SendPaymentRequest{
405+
PaymentRequest: invoice2.PaymentRequest,
406+
TimeoutSeconds: 30,
407+
FeeLimitMsat: 10_000_000,
408+
MaxParts: 1,
409+
},
410+
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
411+
)
412+
413+
// Verify Zane's cached policy for Bob->Alice is still the OLD fee.
414+
// This confirms that ApplyChannelUpdate did NOT update the cache
415+
// from the failure message (the bug).
416+
zaneChanInfo, err = zane.RPC.LN.GetChanInfo(
417+
ctx, &lnrpc.ChanInfoRequest{ChanId: bobAliceChanID},
418+
)
419+
require.NoError(ht.T, err)
420+
421+
if zaneChanInfo.Node1Pub == bob.PubKeyStr {
422+
zaneBobFeeRate = zaneChanInfo.Node1Policy.FeeRateMilliMsat
423+
} else {
424+
zaneBobFeeRate = zaneChanInfo.Node2Policy.FeeRateMilliMsat
425+
}
426+
require.Equal(ht.T, oldBobFeeRate, zaneBobFeeRate,
427+
"bug confirmed: zane's cache was NOT updated from "+
428+
"the payment failure message")
429+
430+
// Clean up.
431+
ht.CloseChannel(zane, chanPointZane)
432+
}

0 commit comments

Comments
 (0)