In this commit, we bound the channel mailbox by message count and by the
encoded size of non-commitment control messages. Commitment updates retain
their full custom-record allowance and remain protected by the count bound.
If either budget fills, we disconnect the peer instead of silently dropping
an ordered channel message.
We also reject unauthorized fee updates before fee-exposure evaluation,
return the exposure error used to fail the link, and emit peer-controlled
warning classes only once per link lifetime.
The interceptor exposes its derived auto-fail height as an int32. Calculate
the height in int64 and fail forwards whose deadline cannot be represented
with expiry_too_far.
Add coverage for the range check and subsequent forward handling.
When the switch forwards a blinded hop identified by node ID, it has not
yet resolved a concrete outgoing channel at interception time. Expose the
next hop to the interceptor: InterceptedForward.Packet() reports the
packet's outgoing channel as-is (hop.Exit, since none is selected yet) and
carries the requested pubkey in OutgoingNodeID.
At the RPC boundary, forwardInterceptor.onIntercept maps a node-ID hop to
the reserved NodeIDForwardSCID sentinel in outgoing_requested_chan_id and
the pubkey in outgoing_requested_node_id, so a client switching on a zero
channel ID to detect the exit hop does not misread the forward as a final
receive. The sentinel is a wire-only concern, applied where the request is
built rather than in the switch's internal InterceptedPacket, which stays
truthful (OutgoingNodeID.IsSome() is the node-ID discriminator).
Now that the switch forwards blinded hops identified by node ID, a new
problem surfaces in the HTLC event stream. A node-ID next hop has no
outgoing short channel ID until non-strict forwarding selects one, so a
forward that fails before selection still carries outgoingChanID ==
hop.Exit. getEventType keys the exit hop off that sentinel, so it
misclassifies such a failed node-ID forward as a receive, mislabeling the
event streamed via SubscribeHtlcEvents (a forwarding failure reported as
a receive failure).
Two paths reach getEventType before an SCID is selected: the fail packet
built by failAddPacket and the resolution packet built by resolve, both
of which dropped the decoded next hop. Carry outgoingHop into both, and
classify a Right (node-ID) outgoingHop as a forward before the hop.Exit
check. A node-ID next hop is always a forward, never the exit hop.
Fixeslightningnetwork/lnd#10937: forward a blinded-route payment when the
recipient identifies the next hop by node ID rather than a short channel
ID. The htlcPacket carries the decoded next hop to the switch, whose
handlePacketAdd resolves the pubkey to the peer's links via getLinks() and
lets the existing non-strict forwarding logic load-balance across the
peer's channels.
outgoingChanID stays a ShortChannelID. It is the persisted CircuitKey and
is set to the selected channel after non-strict selection. The circular
route check filters candidate channels before selection.
Some implementations (e.g. Core Lightning) identify the next hop in a
blinded route by the next node's ID (next_node_id) instead of a short
channel ID. Decode such a hop into a node-ID next hop, the Right of
ForwardingInfo.NextHop, holding the next node's public key. The switch
resolves that key to one of our channels with the peer in a later commit.
BOLT 4 requires a non-final blinded hop to carry exactly one of
short_channel_id or next_node_id, so a hop that sets both is rejected.
The forwarding next hop is currently always a short channel ID. To allow a
blinded route to identify the next hop by node ID instead, change
ForwardingInfo.NextHop to fn.Either[lnwire.ShortChannelID, [33]byte], where
the Left is the outgoing channel ID and the Right (wired up in a follow-up
commit) is the next node's public key.
This commit is a pure representational change with no behavioural effect:
every next hop is still a channel ID. The Either is encapsulated behind
ForwardingInfo methods so callers never destructure it directly: IsExit()
is the single source of truth for exit-hop detection (used by the link and
the contract court) and NextHopChannel() yields the outgoing SCID.
During non-strict forwarding, handlePacketAdd evaluates every candidate
channel to the next peer and calls CheckHtlcForward with the sender-requested
outgoing SCID (originalOutgoingChanID) for each candidate. That SCID flowed
through canSendHtlc into AuxTrafficShaper.ShouldHandleTraffic, so a
channel-keyed shaper was asked about the requested channel rather than the
candidate actually being evaluated. With parallel channels to a peer this
inspects the wrong channel.
Key the shaper on l.ShortChanID() (the channel under evaluation) instead.
originalScid is retained solely for createFailureWithUpdate / FailAliasUpdate,
so the alias-aware channel_update returned to the sender is unchanged and the
real SCID handed to the shaper never leaks onto the wire.
Update htlcswitch test utilities to construct and pass
chanstate.OpenChannel values directly.
This removes another test-only dependency on the channeldb OpenChannel
alias while leaving the test database helpers unchanged.
Move htlcswitch link-facing channel state boundaries to chanstate.
The link still uses channeldb for forwarding-package persistence, but
channel update callbacks, tower registration, and dust helper channel
types now use the channel state package directly.
Move the htlcswitch channel fetch callbacks to chanstate types.
The switch still depends on channeldb for its KV circuit storage and
forwarding package access. This commit only moves channel-state
payloads at the switch and circuit-map boundaries.
Drop the temporary channel type parameter from the channel-state store
interfaces now that OpenChannel lives in chanstate.
The domain store subinterfaces now refer to *OpenChannel directly while
retaining the same backend-independent shape. Update callers and
compatibility aliases to use the concrete Store and ChannelShell types.
Remove the KV forwarding packager from OpenChannel and derive a
ChannelPackager inside the channeldb store methods that need one.
This keeps the backend-specific kvdb transaction helper in channeldb,
so the OpenChannel type no longer carries that dependency toward
chanstate.
Change OpenChannel.Db to the composed chanstate Store interface while
keeping the existing field name.
Tests that need raw channeldb access now assert the concrete test
backend explicitly instead of reaching through OpenChannel.Db. This
keeps backend setup out of the store contract.
Store held forwards as off-chain or on-chain entries instead of a raw
InterceptedForward map. Off-chain entries keep the existing resume, fail,
settle and auto-fail behavior. On-chain entries are settle-only and
expire by pruning local interceptor state.
When contractcourt re-offers a circuit that is already held off-chain,
replace the stored entry with the on-chain forward so a later SETTLE
reaches the witness beacon instead of the old link mailbox path.
Also set the on-chain interceptor deadline to the HTLC refund timeout.
This keeps the public interceptor deadline populated while ensuring only
off-chain held entries use that value to fail back.
Only off-chain held HTLCs can be released when an optional interceptor
disconnects, because they can resume into the link forwarding flow.
On-chain held HTLCs have no link flow to resume. Keep them in the held
set so a reconnecting interceptor can replay and settle them while
contractcourt waits for the preimage or on-chain expiry.
Use distinct internal deadline types for off-chain auto-fail heights and
on-chain settlement deadlines instead of overloading the intercepted packet
field.
Project both variants back into the existing router RPC auto_fail_height
field to preserve wire compatibility. Reject mismatched held HTLC deadline
types in tests.
On-chain intercepted HTLCs can only be settled. Resume and fail actions
already return concrete errors through the on-chain intercepted forward, so
let those errors propagate to the interceptor client instead of converting
them to success.
Keep the held entry tracked on these errors so the client can reconnect and
settle the HTLC later.
Rename the ForwardingInfo.OutgoingCTLV field to OutgoingCLTV and update all
call sites. This keeps the exported field spelling consistent with the CLTV
terminology used elsewhere.
Also fix the remaining CTLV typos in nearby comments.
Apply the same supported CLTV delta range to final-hop HTLC handling that is
already used for forwarding.
Use a shared helper for the exit-hop link path so final-hop amount and CLTV
checks remain consistent across invoice creation and HTLC handling.
Migrate all btcd dependencies to the new per-package v2 modules (wire/v2,
txscript/v2, chaincfg/v2, chainhash/v2, btcutil/v2, psbt/v2, btcec/v2)
introduced by btcd v0.26.0, and pin the tagged ecosystem versions:
btcwallet v0.17.0, neutrino v0.18.0 and lightning-onion v1.4.0.
The bulk of the import rewrite was produced by the scripted diff from
https://github.com/btcsuite/btcd/pull/2547 (followed by 'make rpc'). The
address symbols that moved out of btcutil into the new address package
are imported as btcaddr where a local "address" variable would otherwise
shadow them. The go.mod/go.sum updates and the remaining manual
compilation fixes are folded into this single commit so it builds on its
own (the migration was previously split into a reproducible scripted-diff
plus follow-ups, intended to be squashed on merge).
Since Go 1.22 loop variables are scoped per-iteration, so the
`x := x` / `a, b := a, b` copies inside range/for loops are no longer
needed. This removes the existing redundant copies in test files.
Before this commit, we'd _always_ set both nonces fields, for both the
staging and the final taproot channels type.
With this commit, we've switched to only setting the new nonce map field
for the final taproot feature bit type.
The channelLink.Stop() teardown had an inverted ordering that could
cause a permanent deadlock of the invoice registry under concurrent
peer disconnect.
The previous order was:
1. HodlUnsubscribeAll -- removes subscriptions
2. hodlQueue.Stop() -- kills the queue's internal goroutine
3. cg.Quit() -- signals htlcManager to stop
4. cg.WgWait() -- waits for htlcManager to exit
The race window between steps 2 and 4 left htlcManager alive. A
RevokeAndAck arriving during that window could drive processRemoteAdds
→ processExitHop → NotifyExitHopHtlc, registering a new hodl
subscription backed by a dead hodlQueue (ChanIn() has no reader).
Any subsequent call to notifyHodlSubscribers (e.g. MPP auto-release
timer, expiry watcher, or explicit settle/cancel) would then block
indefinitely on the unbuffered ChanIn(), holding hodlSubscriptionsMux.
Concurrent NotifyExitHopHtlc calls waiting for that lock, plus callers
holding the invoice-level lock waiting for those, produce a full
deadlock of the invoice registry with no recovery path short of a
daemon restart.
The fix is to stop htlcManager before touching the hodl subscription
state. htlcManager is the sole caller of NotifyExitHopHtlc, so once
cg.WgWait() returns no new subscriptions can be registered, making
HodlUnsubscribeAll and hodlQueue.Stop() race-free.
In the previous iteration with endorsement
signaling, the recommendation was for the sender to
set it to 1 and that could have had privacy concerns
when first deploying given that the default was to
downgrade the signal to 0. In the latest proposal
the recommended default for both sending and
forwarding nodes is to set `accountable` to 0.
As a result, the dates have been removed given
that there are no privacy risks associated
with relaying the signal with zero values.
This is the final step, we actually call the interface and either
provide or retrieve the custom features over the message. We also notify
the aux components when channel reestablish is received.
Use the new feature of Go 1.24, fix linter warnings.
This change was produced by:
- running golangci-lint run --fix
- sed 's/context.Background/t.Context/' -i `git grep -l context.Background | grep test.go`
- manually fixing broken tests
- itest, lntest: use ht.Context() where ht or hn is available
- in HarnessNode.Stop() we keep using context.Background(), because it is
called from a cleanup handler in which t.Context() is canceled already.
This commit makes removes the `defaultQuiescenceTimeout` and makes it
configurable as different nodes have different network environment. In
addition the default timeout has been increased from 30s to 60s.