Skip to content

Commit 4d048d1

Browse files
committed
graph: fall back to GraphSource for channel updates from payment failures
When a payment fails due to a stale fee policy (e.g. FeeInsufficient), the router extracts the updated ChannelUpdate from the failure message and calls ApplyChannelUpdate. Previously, this only looked up the channel in the local graph DB, so channels that only existed in a remote graph source (via the Mux) could not be updated, causing the payment to fail with NO_ROUTE. This commit adds a fallback path: when the local DB lookup fails and a GraphSource is configured, ApplyChannelUpdate now queries the GraphSource (which includes the remote graph) to find the channel, validates the update signature, and applies the policy change directly to the graph cache. The router can then retry the payment with the correct fee policy. The integration test is updated to assert the fixed behavior: the payment now succeeds on retry after the cache is updated from the failure message.
1 parent 50b5db5 commit 4d048d1

3 files changed

Lines changed: 101 additions & 45 deletions

File tree

graph/builder.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ type Config struct {
102102
// IsAlias returns whether a passed ShortChannelID is an alias. This is
103103
// only used for our local channels.
104104
IsAlias func(scid lnwire.ShortChannelID) bool
105+
106+
// GraphSource is an optional read-only graph source that may combine
107+
// local and remote graph data. When set, ApplyChannelUpdate will fall
108+
// back to this source to look up channels that don't exist in the
109+
// local graph DB (e.g. channels only known via a remote graph). Policy
110+
// updates for such channels are applied directly to the graph cache
111+
// without a DB write.
112+
GraphSource graphdb.GraphSource
105113
}
106114

107115
// Builder builds and maintains a view of the Lightning Network graph.
@@ -938,7 +946,15 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
938946

939947
ch, _, _, err := b.GetChannelByID(msg.ShortChannelID)
940948
if err != nil {
949+
// If the channel isn't in the local DB but we have a
950+
// GraphSource (e.g. a remote graph), try looking it up there
951+
// and apply the update directly to the graph cache.
952+
if b.cfg.GraphSource != nil {
953+
return b.applyChannelUpdateFromSource(ctx, msg)
954+
}
955+
941956
log.Errorf("Unable to retrieve channel by id: %v", err)
957+
942958
return false
943959
}
944960

@@ -982,6 +998,83 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
982998
return true
983999
}
9841000

1001+
// applyChannelUpdateFromSource handles channel updates for channels that only
1002+
// exist in the GraphSource (e.g. via a remote graph) and not in the local DB.
1003+
// It looks up the channel from the GraphSource, validates the update signature,
1004+
// and applies the policy change directly to the graph cache.
1005+
func (b *Builder) applyChannelUpdateFromSource(ctx context.Context,
1006+
msg *lnwire.ChannelUpdate1) bool {
1007+
1008+
chanID := msg.ShortChannelID.ToUint64()
1009+
1010+
ch, _, _, err := b.cfg.GraphSource.FetchChannelEdgesByID(ctx, chanID)
1011+
if err != nil {
1012+
log.Errorf("Unable to retrieve channel %v from graph "+
1013+
"source: %v", chanID, err)
1014+
1015+
return false
1016+
}
1017+
1018+
var pubKey *btcec.PublicKey
1019+
1020+
switch msg.ChannelFlags & lnwire.ChanUpdateDirection {
1021+
case 0:
1022+
pubKey, _ = ch.NodeKey1()
1023+
1024+
case 1:
1025+
pubKey, _ = ch.NodeKey2()
1026+
}
1027+
1028+
if pubKey == nil {
1029+
log.Errorf("Unable to decide pubkey with ChannelFlags=%v",
1030+
msg.ChannelFlags)
1031+
1032+
return false
1033+
}
1034+
1035+
err = netann.ValidateChannelUpdateAnn(pubKey, ch.Capacity, msg)
1036+
if err != nil {
1037+
log.Errorf("Unable to validate channel update: %v", err)
1038+
1039+
return false
1040+
}
1041+
1042+
update, err := models.ChanEdgePolicyFromWire(chanID, msg)
1043+
if err != nil {
1044+
log.Errorf("Unable to parse channel update: %v", err)
1045+
1046+
return false
1047+
}
1048+
1049+
cache := b.cfg.Graph.GraphCacheStore()
1050+
if cache == nil {
1051+
log.Errorf("No graph cache available for remote channel "+
1052+
"update (chan_id=%v)", chanID)
1053+
1054+
return false
1055+
}
1056+
1057+
fromNode, _ := ch.NodeKey1()
1058+
toNode, _ := ch.NodeKey2()
1059+
1060+
if fromNode == nil || toNode == nil {
1061+
log.Errorf("Unable to get node keys for channel %v", chanID)
1062+
1063+
return false
1064+
}
1065+
1066+
cache.UpdatePolicy(
1067+
models.NewCachedPolicy(update),
1068+
route.NewVertex(fromNode),
1069+
route.NewVertex(toNode),
1070+
)
1071+
1072+
log.Debugf("Applied channel update from graph source for "+
1073+
"chan_id=%v directly to graph cache", chanID)
1074+
1075+
return true
1076+
}
1077+
9851078
// AddNode is used to add information about a node to the router database. If
9861079
// the node with this pubkey is not present in an existing channel, it will
9871080
// be ignored.

itest/lnd_remote_graph_test.go

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

1010
"github.com/btcsuite/btcd/btcutil"
1111
"github.com/lightningnetwork/lnd/lnrpc"
12-
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1312
"github.com/lightningnetwork/lnd/lntest"
1413
"github.com/lightningnetwork/lnd/lntest/node"
1514
"github.com/lightningnetwork/lnd/lntest/wait"
@@ -381,51 +380,14 @@ func testRemoteGraphPolicyUpdate(ht *lntest.HarnessTest) {
381380
require.Equal(ht.T, oldBobFeeRate, zaneBobFeeRate,
382381
"zane should still see old fee rate")
383382

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.
383+
// Now Zane tries to pay Alice again. The first attempt uses the stale
384+
// fee policy, so Bob rejects it with FeeInsufficient and includes the
385+
// updated ChannelUpdate in the failure message. ApplyChannelUpdate
386+
// falls back to the GraphSource (since the channel isn't in Zane's
387+
// local DB), updates the graph cache, and the router retries with
388+
// the correct fees.
395389
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")
390+
ht.CompletePaymentRequests(zane, []string{invoice2.PaymentRequest})
429391

430392
// Clean up.
431393
ht.CloseChannel(zane, chanPointZane)

server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
11121112
AssumeChannelValid: cfg.Routing.AssumeChannelValid,
11131113
StrictZombiePruning: strictPruning,
11141114
IsAlias: aliasmgr.IsAlias,
1115+
GraphSource: s.graphSource,
11151116
})
11161117
if err != nil {
11171118
return nil, fmt.Errorf("can't create graph builder: %w", err)

0 commit comments

Comments
 (0)