[bug]: historical sync stalls after reply_channel_range exceeds 100k SCID cap
Pre-Submission Checklist
- I have searched the existing issues and believe this is a new bug.
- I am not asking a question about how to use lnd, but reporting a bug (otherwise open a discussion).
LND Version
v0.21.3
LND Configuration
lnd.conf: lnd.backend=neutrino
Backend Version
Bitcoin core v31.1
Backend Configuration
OS/Distribution
Bug Details & Steps to Reproduce
Since 2bcb757 ("discovery: bound channel range reply buffering", v0.21.2-beta) a GossipSyncer aborts a query_channel_range once the aggregate reply exceeds maxChanRangeReplySCIDs = 100_000. The bound is fine, but the error path leaves the node without any active historical sync and it never recovers on its own:
channelGraphSyncer(discovery/syncer.go,waitingQueryRangeReplycase) logsUnable to process chan range query: …and does a barereturn. The goroutine exits but theGossipSyncerstays registered asinitialHistoricalSyncerand the peer stays connected.SyncManageronly replaces the initial historical syncer on peer disconnect (staleSyncerpath). No disconnect happens here, so nothing is replaced.- While
IsGraphSynced()is false, every newly connected peer is created as a passive syncer and parked ininactiveSyncers"until the initial historical sync completes", which can no longer happen. - The only remaining trigger is
HistoricalSyncTicker(default 1h), which picks a random syncer and can re-pick the same oversized peer.
Result: one peer with a bloated graph view (zombie channels, see #10033) leaves a freshly restarted node at synced_to_graph: false for an hour or more. A restart was needed to recover.
Environment
lnd v0.21.3-beta, mainnet, numgraphsyncpeers default (3), gossip.ban-threshold default. The same node on v0.21.1-beta synced fine against the same peers.
Steps to reproduce
- Run lnd ≥ v0.21.2 and make sure the initial historical sync is performed against a peer whose
reply_channel_rangeforfirst_block=0, num_blocks=<tip>carries more than 100,000 SCIDs. Easiest isnobootstrap=1pluslncli connect <peer>so it is the only candidate, orgossip.pinned-syncers=<pubkey>. The peer that triggered it here is a well-connected public mainnet node (pubkey available on request). - Restart lnd so a fresh initial historical sync runs. In this case nothing was forced: the first pick was banned mid-sync and
SyncManagerfailed over to this peer on its own. - Observe
[ERR] DISC: Unable to process chan range query: channel range reply exceeds maximum number of short channel IDs: max=100000, then no furtherGossipSynceractivity,getinforeportssynced_to_graph: false, and only a trickle ofchannel_updates arrives via passive syncers.
Expected behaviour
A processChanRangeReply error should be handled like a disconnect of the initial historical syncer: notify SyncManager, run forceHistoricalSync() against a different peer immediately, and exclude the failing peer from that pick. Secondary: make the SCID cap configurable or derive it from the local graph size, since peers serving zombie-heavy graphs exceed 100k today while the public graph is ~30k channels.
Actual behaviour
Syncer goroutine exits, no failover, node unsynced for ~60 minutes until restarted. After the restart a different peer served 31,215 SCIDs and the sync completed in 18 seconds.
Logs (UTC, 2026-09-04)
Peers are labelled PEER_A … PEER_E. PEER_C is the one whose reply exceeds the cap.
First historical sync, killed by a ban mid-query. lnd correctly fails over here because the peer disconnected:
12:11:08.258 [INF] LTND: Chain backend is fully synced! end_height=965474
12:11:11.573 [INF] DISC: Creating new GossipSyncer for peer=PEER_A
12:11:11.574 [INF] DISC: GossipSyncer(PEER_A): requesting new chans from height=0 and 965474 blocks after
12:11:16.864 [INF] DISC: Creating new GossipSyncer for peer=PEER_B
12:11:17.162 [INF] DISC: Creating new GossipSyncer for peer=PEER_D
12:11:17.214 [INF] DISC: Creating new GossipSyncer for peer=PEER_C
12:11:19.368 [INF] DISC: Creating new GossipSyncer for peer=PEER_E
12:11:29.758 [INF] DISC: GossipSyncer(PEER_A): buffering chan range reply of size=4000
… 7 more replies of ~4000 …
12:11:44.183 [INF] DISC: GossipSyncer(PEER_A): buffering chan range reply of size=539
12:11:44.183 [INF] DISC: GossipSyncer(PEER_A): filtering through 29159 chans
12:11:45.149 [INF] DISC: GossipSyncer(PEER_A): starting query for 565 new chans
12:11:45.149 [INF] DISC: GossipSyncer(PEER_A): querying for 500 new channels
12:11:52.070 [INF] PEER: Peer(PEER_A): disconnecting PEER_A@<addr>, reason: peer has bypassed ban threshold - banning
12:11:52.172 [INF] DISC: Removing GossipSyncer for peer=PEER_AFailover to the next peer, which trips the cap. This is where the syncer dies:
12:11:52.181 [INF] DISC: GossipSyncer(PEER_C): requesting new chans from height=0 and 965474 blocks after
12:11:57.262 [INF] DISC: GossipSyncer(PEER_C): buffering chan range reply of size=0
12:11:57.627 [INF] DISC: GossipSyncer(PEER_C): buffering chan range reply of size=4000
12:11:57.805 [INF] DISC: GossipSyncer(PEER_C): buffering chan range reply of size=4000
… 22 more replies of 3988–4000 within 1.4s …
12:11:58.706 [INF] DISC: GossipSyncer(PEER_C): buffering chan range reply of size=3996
12:11:58.706 [ERR] DISC: Unable to process chan range query: channel range reply exceeds maximum number o00Nothing further from the sync manager for the next hour. The only GossipSyncer line in 60 minutes is a new peer being parked as passive:
13:01:40.160 [INF] DISC: GossipSyncer(PEER_B): applying new remote update horizon: start=2026-09-04 13:01 19:29:55 +0000 UTC, has_updates=trueGraph activity in that window: ~10 GRPH: Received ChannelEdgePolicy lines between 12:24 and 13:01. A healthy node logs GRPH: Processed channels=… updates=… every minute.
Manual restart, successful sync from a different peer with the same query shape:
13:12:01.996 [INF] LTND: Version Info version=0.21.3-beta
13:14:08.131 [INF] LTND: Chain backend is fully synced! end_height=965480
13:15:58.472 [INF] DISC: GossipSyncer(PEER_E): buffering chan range reply of size=3999
… 6 more …
13:16:05.279 [INF] DISC: GossipSyncer(PEER_E): buffering chan range reply of size=1772
13:16:05.279 [INF] DISC: GossipSyncer(PEER_E): filtering through 31215 chans
13:16:07.430 [INF] DISC: GossipSyncer(PEER_E): starting query for 492 new chans
13:16:16.254 [INF] DISC: GossipSyncer(PEER_E): no more chans to query
13:16:16.254 [INF] DISC: GossipSyncer(PEER_E): applying gossipFilter(start=2026-09-04 13:16:16 …)294 GRPH: Received lines in the following 2.5 minutes.
For comparison, the same PEER_C against a second node still on v0.21.1-beta earlier that day:
05:52:11.735 [INF] DISC: GossipSyncer(PEER_C): requesting new chans from height=0 and 965432 blocks after
05:52:20.371 [INF] DISC: GossipSyncer(PEER_C): filtering through 29631 chansSame peer, ~29.6k SCIDs at 05:52 vs >100k at 12:11. Whether that is the peer's state or a difference in h8af890e lnwire+discovery: handle zero-block ranges is the only other syncer.go change between theversions) I can't tell from this side. The failure mode after the cap is the bug being reported regardless.
Expected Behavior
Perhaps some mitigations:
- Try a new peer if the first one gets banned, up to 5 peers.
- Reduce the default of 1 hour
Debug Information
No response
Environment
No response
Source: lightningnetwork/lnd