Compare commits

...

1274 commits

Author SHA1 Message Date
Olaoluwa Osuntokun
8ea98fd522
Merge pull request #11066 from starius/fix-docker-release
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
docker-release: fix running from Git worktree and if Go cache dir is missing
2026-08-12 18:55:40 -07:00
Boris Nagaev
ff98891ffe
build: support Docker releases from Git worktrees
Mount a linked worktree's shared Git directory into the release helper
at its original path.

Its .git pointer otherwise resolves to a path absent from the container,
preventing tag checks and source archiving.
2026-08-13 00:20:10 +00:00
Boris Nagaev
f2a7073106
build: initialize Docker release caches
Create and validate host cache directories before Docker bind mounts them.

On a fresh Go installation, Docker otherwise creates missing sources as
root. The release helper runs as the invoking user and cannot write
to those caches.
2026-08-13 00:20:10 +00:00
Yong
8464116a05
Merge pull request #11057 from starius/add-boris-key
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
scripts/keys: add pub key for boris
2026-08-11 13:23:32 +08:00
Boris Nagaev
69176c5d18
scripts/keys: add pub key for boris 2026-08-10 15:57:48 -05:00
ziggieXXX
d932f48678
Merge pull request #11046 from ziggie1984/add-0.20.2-0.20.3-notes-to-master
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
docs: add 0.20.2 and 0.20.3 release notes to master
2026-08-07 16:18:13 -03:00
ziggie
bd6e91a369
docs: add 0.20.2 and 0.20.3 release notes to master
Master carries the release notes for 0.20.0 and 0.20.1, and the 0.21
line is complete through 0.21.2, but the 0.20 patch notes stopped
being forward-ported after 0.20.1. Copy release-notes-0.20.2.md and
release-notes-0.20.3.md over from v0.20.x-branch so master holds the
full historical record.

Both files are byte-identical to their counterparts on the release
branch, and the 0.20.2 notes also match the content published at the
v0.20.2-beta tag.
2026-08-07 13:55:36 -03:00
Yong
7f56541dc9
Merge pull request #11023 from ellemouton/update-fee-log
Some checks failed
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
Vulnerability scan / Scan release binaries (push) Has been cancelled
lnwallet+htlcswitch: align update_fee handling with BOLT 2
2026-08-07 12:30:57 +08:00
Elle Mouton
ef24f2c5c3
htlcswitch: bound peer-controlled channel ingress
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.
2026-08-06 19:24:19 -07:00
ziggieXXX
99457a272c
Merge pull request #11028 from ziggie1984/cltv-interceptor-deadline-range
htlcswitch: validate intercepted auto-fail height
2026-08-06 13:16:20 -03:00
ziggieXXX
18f930a5f2
Merge pull request #11032 from ziggie1984/contractcourt-forward-contest-deadline
contractcourt: retain deadline across contest resolution
2026-08-06 08:14:51 -03:00
ziggie
5b4c5067db
docs: update 0.21.2 release notes 2026-08-05 23:33:12 -03:00
ziggie
f77606851f
contractcourt: retain deadline across contest resolution
Forward the supplied incoming HTLC expiry from the outgoing contest resolver to its embedded timeout resolver. This keeps the deadline available when resolution transitions after the outgoing HTLC expires.
2026-08-05 23:31:51 -03:00
Yong
f4ae565619
Merge pull request #11019 from Roasbeef/coop-close-race-public
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
lnwallet/chancloser: fix data race in the legacy coop close state machine
2026-08-06 09:25:18 +08:00
ziggie
4ccb2aa94a
docs: add interceptor auto-fail height release note 2026-08-05 19:18:58 -03:00
ziggie
ae3f4aff48
htlcswitch: bound intercepted auto-fail height
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.
2026-08-05 19:14:49 -03:00
Olaoluwa Osuntokun
4944bb0794 docs: add release notes entry for the coop close fixes 2026-08-05 12:23:13 -07:00
Olaoluwa Osuntokun
fb89732d24 lnwallet/chancloser: record the remote close output only when accepted
In this commit, we hold off on recording the remote party's close output until
we've decided we can act on their Shutdown. ReceiveShutdown wrote the field
before it looked at the state, so a Shutdown that arrives at a point where we
have nothing to do with it, say once we've already finished the negotiation,
would still overwrite the output we settled on before being turned away with
ErrInvalidState. The output we report for the close then describes a message we
rejected.

Nothing acts on this today, as we hand the outputs to the caller only after
ClosingTx tells it the negotiation finished, but the field is what we report to
the party that asked for the close, so we may as well only fill it in from a
message we accepted.
2026-08-05 12:22:56 -07:00
Olaoluwa Osuntokun
e5e134ddac peer+lnwallet/chancloser: advance the legacy closer from one goroutine
In this commit, we give the legacy ChanCloser a single owner, rather than
letting two goroutines advance it. The peer's channelManager drives the state
machine for the Shutdown and ClosingSigned messages that come off the wire, and
for local close requests. The link drives it as well: while we wait for the
channel to drain we register a flush hook, and the link invokes that hook from
its own goroutine, where it called BeginNegotiation directly. Nothing kept the
two apart, so the state field, the priorFeeOffers map, and the signing step
could all be touched at once. Under `go test -race` this shows up as a data race
on the state field.

Rather than reach for a lock, we route the flush through the channelManager. The
hook now only reports the channel ID over a new chanCloseFlushed channel, and
handleChanFlushed picks it up next to the close messages. Every transition, the
cached offer processing, the fee map, and the signing then happen on the one
goroutine, so the closer needs no synchronization of its own. We spell that out
on the type, since it's an invariant a new caller can break from the outside.

The report goes out from a fresh goroutine, which matters more than it looks.
The link may well be holding its own lock while it invokes the hook, and
channelManager reaches for that same lock in DisableAdds, so blocking on the
handoff would trade the race for a deadlock. The `go` in front of RemoveLink
just above it is there for the same reason.

We look the closer up with a plain map load rather than through
fetchActiveChanCloser, as that one builds a fresh closer when it doesn't find
an existing one, and a flush that lands after the negotiation was torn down has
no business starting a new negotiation.

One behavior change falls out of the move: the flush path now runs the same
finalization tail as the message path. It skipped that before, so a responder
that drained a cached offer would reach closeFinished and broadcast, but nothing
ran finalizeChanClosure until the next close message showed up, and having
already sent its final signature, there may not be one. The link == nil path
already ran the tail, so this makes all three paths agree.

The new test drives a close with a link that hands us the flush hook instead of
running it inline, so we can check that negotiation waits on the report, and
that a report for a channel we have no closer for is dropped.
2026-08-05 12:22:56 -07:00
Olaoluwa Osuntokun
a8e2a0f7fa lnwallet/chancloser: validate delivery scripts in the RBF closer
In this commit, we make the RBF co-op closer validate the remote party's
delivery script in all cases, matching what the negotiation closer already does.
Previously we only ran the check when we had an upfront shutdown script on record
for the peer, so a peer that never committed to an upfront script could hand us a
delivery script that we'd stash and carry through the rest of the close flow
without ever looking at it.

We now always call validateShutdownScript with the (possibly nil) upfront
script: a nil upfront script still runs the well-formedness check on the peer's
script, and a non-nil one additionally enforces the exact match, same as before.
We also require the script to be present. The wire format puts no lower bound on
the address length, and validateShutdownScript treats an absent peer script as
nothing to check, so an empty one passed validation by default rather than on its
merits. Both entry points now go through one helper that insists on a script
before running the usual checks over it, which also covers a CloserScript
swapped in mid-negotiation via ClosingComplete rather than letting that one go
unchecked.

The delivery-form coverage is spelled out in the tests: the spec dropped p2pkh
and p2sh for co-op closes to keep the dust calculations uniform, and we don't
implement the OP_RETURN form that option_simple_close allows, so all of those are
rejected along with an empty or malformed script.
2026-08-05 12:22:56 -07:00
Olaoluwa Osuntokun
f80f92dc04 lnwallet: make DustLimitForSize total over the sizes it can be handed
In this commit, we have DustLimitForSize fall back to the generic witness dust
threshold for any script size that doesn't match one of the well-known
templates.

The size switch covered P2WPKH, P2WSH, P2SH, P2PKH, and the explicit
unknown-witness size, and treated every other length as unreachable. That's a
narrower assumption than the callers can actually make good on: a witness
program for versions 1 through 16 carries a program of anywhere from 2 to 40
bytes, so its serialized length won't always land on one of those exact values.

The dust calculation only needs a representative output of roughly the right
shape, and the unknown-witness pricing is the conservative choice among the
ones we have, so we make it the default. That leaves the helper well defined
across the whole range of sizes callers can pass it, including scripts carrying
witness versions we don't know about yet.
2026-08-05 12:22:56 -07:00
ziggieXXX
61f3b5a74f
Merge pull request #11024 from ziggie1984/invoices-update-validation
invoices: refine update handling
2026-08-05 15:13:14 -03:00
ziggie
758bbb8e52
docs: update 0.21.2 release notes 2026-08-05 08:28:51 -03:00
ziggie
6be6350ec4
invoices: refine update handling 2026-08-05 08:28:51 -03:00
Elle Mouton
6711f758fd
lnwallet: coalesce uncommitted fee updates
In this commit, we bound the resident fee update log without imposing a
fixed update limit. We replace the value of the newest fee update while
it remains uncommitted on both commitment chains. Once either chain
observes the update, we retain it so retransmission and intermediate
commitment states keep their existing semantics.

We also cover generated interleavings, sustained remote update streams,
committed-state preservation, and the legacy fee-update restore path.
2026-08-04 16:15:10 -07:00
Olaoluwa Osuntokun
afdfd19484
Merge pull request #11018 from ziggie1984/move-10992-release-note-to-v0.21.2
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
docs: move gossip bounds note to v0.21.2
2026-08-03 15:20:44 -07:00
ziggie
83e7eb8c08
docs: move gossip bounds note to v0.21.2 2026-08-03 19:18:05 -03:00
ziggieXXX
eee742f269
Merge pull request #10992 from lightningnetwork/gossip-range-bounds
discovery: bound channel range reply memory usage
2026-08-03 17:41:17 -03:00
Yong
1af0f20238
Merge pull request #10955 from lightningnetwork/ci/workflow-job-split
ci: split the Claude PR-severity and issue-dedupe workflows into two jobs
2026-08-04 03:16:06 +08:00
Olaoluwa Osuntokun
ceff94fadd discovery: bound channel range reply buffering
In this commit, we cap each QueryChannelRange response at 100,000 SCIDs
across all streamed replies. The existing reply-count limit did not track
the aggregate decoded working set, so memory use varied with the encoding
and composition of the reply stream.

We count raw SCIDs before timestamp filtering, charge replies using the
received encoding type, and release all accumulated range state on any
error. This bounds both memory and CPU work while still leaving headroom
above the current graph.
2026-07-30 15:19:26 -07:00
Yong
f4a444184d
Merge pull request #10942 from bitromortac/2606-fix-10937-a
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
htlcswitch: forward blinded payments addressed by node_id
2026-07-30 20:30:59 +08:00
bitromortac
f42b429899
docs: update release notes 2026-07-30 09:03:45 +00:00
bitromortac
da6a40c01d
itest: cover blinded route next_node_id forwarding
Add integration tests for an lnd introduction node forwarding a blinded
payment whose non-final hops identify the next hop by node ID (next_node_id)
rather than a short channel ID, as produced by other implementations:

  - testBlindedRouteNextNodeID: the outgoing channel is public.
  - testBlindedRouteNextNodeIDPrivateChannel: the outgoing channel is
    private, so the node ID resolves to an SCID alias.
  - testBlindedRouteNextNodeIDRestart: the introduction node is restarted
    while the HTLC is in flight, exercising forwarding-package replay and
    re-decode of the node-ID blinded hop.
2026-07-30 08:57:19 +00:00
bitromortac
9c4b8bfec2
witness beacon: report node-ID next hop to the on-chain HTLC interceptor
Extend the on-chain interceptor path in the witness beacon to expose a
node-ID next hop, mirroring the off-chain path. A node-ID next hop has no
outgoing channel of its own, so the beacon reports hop.Exit as the outgoing
channel (via ForwardingInfo.NextHopChannel().UnwrapOr) and the requested
next node's public key. The RPC boundary maps that to the NodeIDForwardSCID
sentinel so the forward is not misread as a final receive.

This is the requested next hop, not the channel eventually selected by
non-strict forwarding, so the beacon deliberately does not resolve it
against the circuit map.
2026-07-30 08:57:19 +00:00
bitromortac
32373b76c7
htlcswitch+lnrpc: report node-ID next hop to the off-chain HTLC interceptor
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).
2026-07-30 08:57:18 +00:00
bitromortac
a4844ef522
htlcswitch: classify a node-ID forward as a forward event
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.
2026-07-30 08:57:18 +00:00
bitromortac
dbc5704070
htlcswitch: forward node-ID blinded hops via non-strict forwarding
Fixes lightningnetwork/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.
2026-07-30 08:57:18 +00:00
bitromortac
4fd4289a08
htlcswitch/hop: decode next_node_id blinded hops
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.
2026-07-30 08:54:35 +00:00
Olaoluwa Osuntokun
d162291941 lnwire: cap decoded short channel IDs
In this commit, we cap each decompressed short channel ID set at 100,000
entries, matching the aggregate range reply budget. The old zlib reader
bounded compressed input rather than decoded output, so the two working-set
limits could drift apart.

We retain compatibility with protocol-valid compressed replies, reject
truncated or corrupt zlib streams, and close the reader on every exit.
Boundary, compatibility, corruption, and property tests cover the
decoder.
2026-07-29 19:16:55 -07:00
bitromortac
d28a71765b
multi: represent the blinded forwarding next hop as an fn.Either
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.
2026-07-29 14:02:00 +00:00
bitromortac
14640a5016
lnrpc/routerrpc: add outgoing_node_id to HTLC intercept request
A blinded route may identify the next hop by node ID (next_node_id) rather
than by channel, in which case there is no sender-specified outgoing channel
to report to an HTLC interceptor. Add an outgoing_node_id field to
ForwardHtlcInterceptRequest to carry the next hop's public key for these
forwards, and document that outgoing_requested_chan_id then holds a reserved
sentinel value so that clients switching on a zero channel ID to detect the
exit hop do not misclassify the forward as a final receive.

This commit only adds the schema and regenerated stubs; the fields are
populated by later commits.
2026-07-29 14:02:00 +00:00
bitromortac
b166780015
htlcswitch: key the aux traffic shaper on the evaluated channel
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.
2026-07-29 12:16:31 +00:00
ziggieXXX
530dca19b3
Merge pull request #11000 from saubyk/gateway-v0.6.0
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
github: bump gateway code-review to v0.6.0
2026-07-28 13:46:20 -03:00
Suheb
17a4d42fda github: bump gateway code-review to v0.6.0
Bumps both pins together: the gateway-action SHA and the runtime_ref it
resolves. runtime_ref is pinned explicitly rather than left to the
action's default, so bumping only the action would leave the job on the
v0.5.0 runtime.

v0.6.0 adds no trigger and no input, so the rest of the shim is
unchanged.
2026-07-27 20:38:47 -07:00
ziggieXXX
2c3e6ffcce
Merge pull request #10958 from bitromortac/2604-bolt12-1d
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
bolt12: add invoice_error message for onion message replies
2026-07-24 14:16:13 -03:00
ziggieXXX
dcacdfe0e1
Merge pull request #10941 from bitromortac/2604-bolt12-1c
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
bolt12: add Invoice codec and structural validators
2026-07-23 14:23:38 -03:00
Olaoluwa Osuntokun
d7d2aad11d
Merge pull request #10985 from ellemouton/fix-missing-channeldb-version
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
channeldb: recover missing db version key
2026-07-22 13:46:28 -07:00
Elle Mouton
68264c70d9
channeldb: recover missing db version
Use strict metadata reads during migration selection so a metadata bucket with a
missing metadata/dbp key is not interpreted as the latest DB version.

Recover this state from mandatory DB version 33, the last mandatory version
before the v0.20.x releases that could initialize a DB without writing the DB
version key. This runs migration 35 without replaying migrations 0 through 33
against a DB that was already created by a modern schema/code path.

After the selected migrations complete, syncVersions writes the latest DB
version as usual.
2026-07-22 11:44:01 -07:00
Elle Mouton
3aff61aebe
channeldb: make waiting proof migration repeatable
Allow migration 35 to skip records that are already keyed using the typed
waiting proof format. This lets the missing-version recovery path safely run
migration 35 on DBs that were created directly by v0.21 and may already contain
typed waiting proofs.

Legacy 9-byte records are still migrated and unexpected key shapes still fail.
2026-07-22 11:43:45 -07:00
Elle Mouton
619913bfb2
channeldb: preserve db version during init
Keep the top-level bucket creation introduced by PR #9653, since initialized
DBs can still be missing newer buckets such as the historical channel bucket.

Do not let the metadata bucket created during init make a fresh DB look
initialized. Use strict metadata reads so a missing metadata/dbp key is distinct
from a present DB version, then write metadata/dbp for genuinely fresh DBs.

Existing DBs with a metadata bucket but missing metadata/dbp are left for the
migration recovery path instead of being treated as latest.
2026-07-22 11:43:45 -07:00
ziggieXXX
0f4b9c9a18
Merge pull request #10982 from ziggie1984/payments-fix-orphaned-blinded-total
paymentsdb: normalize orphaned blinded total
2026-07-22 13:53:57 -03:00
bitromortac
5fcdc16a84
docs: update release notes 2026-07-21 16:23:04 +00:00
bitromortac
e279cdf1f2
bolt12: add InvoiceError onion message replies
InvoiceError is the negative-reply counterpart to an invoice, sent over
onion messages at namespace type 68 when the receiver rejects an invoice
request or the sender rejects a returned invoice. All three fields are
odd (informational): erroneous_field (TLV 1, the offending TLV type),
suggested_value (TLV 3, a valid replacement), and error (TLV 5, a UTF-8
explanation). Unlike Offer/InvoiceRequest/Invoice this type has no
bech32 form and no Merkle signature — it travels only inside onion
message payloads.
2026-07-21 16:23:04 +00:00
ziggie
17ee638e37
docs: add payment migration release note
Document the handling of historical total-only blinded route data and the
new SendToRouteV2 validation that prevents creating such records.
2026-07-20 16:28:42 -03:00
ziggie
04da2fa583
paymentsdb: normalize orphaned blinded total
The KV route format stores blinded fields independently. Routes accepted
through SendToRouteV2 could therefore contain a blinded total amount
without encrypted recipient data. The SQL migration treated the total as
proof of a blinded hop and bound nil to the required encrypted-data
column, preventing LND from starting.

Use encrypted recipient data as the blinded-hop discriminator and
normalize only the known total-only case. Reject blinding-point-only
records with payment, attempt and hop context instead of exposing an
opaque SQL constraint error. Log normalized totals, account for them
during migration validation, and cover both cases with regression tests.
2026-07-20 16:28:42 -03:00
ziggie
ffc62509b6
routerrpc: require encrypted blinded hop data
SendToRouteV2 accepts caller-provided routes. It already required
recipient-encrypted data when a blinding point was present. However, it
copied a blinded total amount independently. This allowed a total-only
hop to enter the payment database even though LND did not classify it
as blinded.

Require encrypted data when either blinded field is supplied. Cover the
rejected combination, a valid blinded total and a regular hop.
2026-07-20 16:19:07 -03:00
ziggieXXX
046356759a
Merge pull request #10978 from ziggie1984/kvdb-bulk-read-committed
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
kvdb/sqlbase: relax bulk migration isolation
2026-07-17 22:57:47 -03:00
ziggie
18bbdb5363
kvdb/sqlbase: relax bulk migration isolation 2026-07-17 21:57:20 -03:00
Olaoluwa Osuntokun
f02cf4c470
Merge pull request #10965 from ziggie1984/sqlbase-migration-bulk
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
kvdb: add Postgres migration bulk support
2026-07-16 20:39:42 -05:00
ziggie
d12a8c3af5
kvdb: test migration bulk operations 2026-07-16 12:51:57 -03:00
ziggie
9f97c49adf
kvdb/sqlbase: add postgres migration bulk support
This commit implements MigrationBulkKVStore for Postgres/pgx. The
Postgres wrapper is available through an explicit constructor, so
regular Postgres and shared SQLite backends do not expose the migration
capability accidentally.

The bulk load transaction pins a dedicated *sql.Conn. InsertLeaves streams
rows through pgx COPY inside that transaction. The copied row count is
checked against the input to catch partial loads. Bucket rows are inserted
individually with RETURNING id so nested buckets can reference their parent.

Verification uses a read-only repeatable-read transaction. It fetches
children of a parent-id batch with a native pgx bigint-array and a single
ANY($1) query.

Migration transactions honor the WithTxLevelLock used by regular
transactions. Loads take the write lock and verification takes the
read lock. Commit and Rollback release both the lock and the dedicated
connection. Rollback is idempotent and tolerates an already-closed
transaction.
2026-07-16 12:51:56 -03:00
ziggie
5246c15f18
kvdb/sqlbase: define migration bulk KV interfaces
This commit introduces a migration-only interface set that lets the
KV-to-SQL migration load and verify the raw SQL KV schema directly,
bypassing the walletdb/kvdb bucket abstraction. Normal application code
continues to use the bucket APIs; these helpers exist solely to make the
one-time bulk migration fast and verifiable.

MigrationBulkKVStore is the entry point. It exposes CheckEmpty to guard
against migrating into a populated table, TruncateTargetTable to recover
from an interrupted fresh-only attempt, and two transaction openers:
BeginBulk for loading and BeginBulkVerify for batched verification.

The write path inserts buckets one at a time to obtain generated ids.
It inserts leaves in batches, leaving the concrete bulk strategy to the
backend. The read path walks the tree level with FetchTopLevel and
FetchChildren.

MigrationBulkChild uses an explicit IsBucket flag rather than inspecting
the value column. This prevents an empty leaf value from being confused
with the SQL NULL marker used for buckets.

The interfaces use the same build constraints as the SQL kvdb backends.
Backends expose the migration capability explicitly; the first concrete
implementation is Postgres-only.
2026-07-16 12:51:56 -03:00
ziggieXXX
6f65644448
Merge pull request #10962 from jtobin/rbf-guard
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
peer: never use RBF coop close for aux channels
2026-07-14 16:05:24 -03:00
Jared Tobin
0501731c66
docs: add release notes for v0.21.2 2026-07-14 13:24:40 -02:30
Jared Tobin
ceff3ceb4b
peer: never use RBF coop close for aux channels
The RBF coop close flow was selected purely from the peer-level
feature bits (rbfCoopCloseAllowed), with no per-channel exclusion.
The RBF close state machine does not invoke any of the aux closer
hooks: the Shutdown message it sends carries no aux custom records,
and the close transaction it negotiates contains no aux outputs. For
a taproot asset (overlay) channel this means the funding output --
which anchors the asset commitment -- is spent by a transaction that
does not re-commit the assets, irrevocably destroying them on-chain.
The aux closer then fails to finalize the confirmed close (it was
never asked to produce vPackets), which blocks the chain watcher's
coop close handler and leaves the channel stuck in waiting-close.

See lightninglabs/taproot-assets#2196 for an instance of this
happening in the wild.

Extend rbfCoopCloseAllowed to take the channel type: it now requires
the RBF feature bits AND that the channel type carries no tapscript
root, and is used at every site that chooses between the RBF closer
and the legacy negotiate closer. The RBF close actor's own eligibility
check is dropped entirely: an actor is only ever registered after
initRbfChanCloser has vetted the channel, so the check was redundant.
Aux channels now always fall back to the legacy closer, which is
aux-aware, regardless of the negotiated feature bits. Since no RBF
msg-router endpoint is registered for aux channels, an incoming Shutdown
from the peer likewise falls through to the legacy close handling. As a
backstop, initRbfChanCloser now refuses to construct an RBF closer for
aux channels outright.
2026-07-14 13:24:33 -02:30
ziggieXXX
02f4116926
Merge pull request #10963 from elnosh/fix-networkinfo
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
rpcserver: fix stats undercount in GetNetworkInfo
2026-07-13 11:31:07 -03:00
elnosh
9ccd305692 rpcserver: fix stats undercount in GetNetworkInfo
On an already-seen channel, the loop returned from the whole node
callback instead of continuing, skipping the node's remaining
channels, this undercounted the stats.
2026-07-13 09:29:09 -04:00
Yong
404f2c27ab
Merge pull request #10956 from saubyk/gateway-v0.5.0
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
github: bump gateway code-review to v0.5.0
2026-07-11 00:23:59 +08:00
ziggieXXX
e9a8b3f9c8
Merge pull request #10897 from jtobin/htlc-force-close-sweeper
Some checks failed
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
Vulnerability scan / Scan release binaries (push) Has been cancelled
sweep: account for aux extra budget when filtering inputs
2026-07-09 09:36:04 -03:00
ziggieXXX
fb823f4cab
Merge pull request #10948 from bitromortac/2607-fix-onionmessage
lnwire: enforce bolt 4 rules for onion message final-hop payloads
2026-07-09 08:33:45 -03:00
Jared Tobin
3ae31b7612
docs: add release note 2026-07-09 08:52:50 -02:30
Jared Tobin
a9e3e9ae17
sweep: account for aux extra budget when filtering inputs
The BudgetAggregator filters out inputs whose budget cannot cover the
min relay fee or their requested starting fee rate. For inputs that
carry a resolution blob (custom channel outputs), the aux sweeper
contributes a sizable extra budget to any input set they join, but the
filter only considered the input's own budget, which for asset outputs
is tiny (their value is carried off-chain).

The filter is mostly harmless with default parameters, but the
starting fee rate of an input is ratcheted whenever a sweep attempt
fails, including failures that have nothing to do with fees: e.g. when
a concurrent sweep transaction spends the wallet UTXO that was backing
this input's set (the sweeper currently doesn't lease selected wallet
UTXOs, so concurrent input sets can pick the same one). One such
collision is enough to push the required starting fee above a small
asset input's own budget, after which the input is filtered out of
every future input set and the sweep is silently stranded forever.

Account for the aux extra budget in the filter, mirroring how the
budget input set itself accounts for it when deciding whether wallet
inputs are needed. Inputs without a resolution blob (the only kind
that exists without an aux sweeper) are unaffected.
2026-07-09 08:52:50 -02:30
bitromortac
1ed6719b68
docs: add BOLT 12 invoice release notes
Add release notes for the BOLT 12 invoice codec.
2026-07-09 08:28:43 +00:00
bitromortac
e0f04f17f1
bolt12: validate Invoice messages
Implement the structural validators for the BOLT 12 invoice, adding
ValidateInvoiceWrite, ValidateInvoiceRead, ValidateInvoiceExpiry, and
ValidateInvoiceAgainstRequest.

The validators implement the spec writer and reader requirements in the
order the spec lists them. The reader confirms the signature TLV is
present but defers actual Schnorr verification until the merkle and
signing primitives land, mirroring the ValidateInvoiceRequestRead
precedent.
2026-07-09 08:28:43 +00:00
bitromortac
68bc76912f
docs: update release notes 2026-07-09 07:15:46 +00:00
bitromortac
233e377731
lnwire: reject onion message payloads with multiple final hop fields
BOLT 4 requires the final node to ignore an onion message whose
onionmsg_tlv contains more than one payload field, where payload fields
are the tlv types reserved for the final hop (type 64 and above). Decode
previously accumulated every such field it found, so a payload bundling
invoice_request, invoice, and invoice_error together was accepted.

Reject the payload when more than one final hop field is present. Every
entry collected in FinalHopTLVs is in the final hop range, so its count
is the number of payload fields. The round-trip test for multiple fields
becomes a rejection test, and the property test now draws at most one
payload field.
2026-07-09 07:13:35 +00:00
bitromortac
33c510fa3a
lnwire: reject onion message payloads with unknown even types
BOLT 4 requires the final node to ignore an onion message whose
onionmsg_tlv contains an unknown even type, since even types are
"must understand". The TLV stream decoder does not enforce this on its
own: its parsed-type map collects unknown types of either parity, so an
even type such as 70 would otherwise be accepted as a final hop payload.

Reject any unknown even type during decode, regardless of its range. The
check runs before the final hop range skip so unknown even types below
type 64 are rejected as well.
2026-07-09 07:13:35 +00:00
bitromortac
bc5cfb5adc
lnwire: preserve unknown odd zero-length final hop TLVs
When decoding an onion message payload, the loop that forwards
unrecognized final hop TLVs to higher layers skipped any entry with a
zero-length value. DecodeWithParsedTypesP2P marks a recognized type with
a nil map entry but records the raw bytes for an unknown type, and an
unknown odd TLV with an empty value is valid. Keying the skip off a
length check therefore dropped such a TLV instead of passing it through.

Test the recognized-type skip against a nil entry so a valid unknown odd
zero-length TLV is preserved.
2026-07-09 07:13:35 +00:00
bitromortac
efbeef5833
bolt12: add Invoice struct and TLV codec
Add the BOLT 12 Invoice message: a struct mirroring the invoice_request
fields (types 0-91) plus the invoice-specific fields (types 160-176) and
the signature (type 240), together with its pure-TLV Encode/DecodeInvoice
codec and the UsableFallbackAddresses accessor that applies the spec's
MUST-ignore filter.

Additionally, add the NewInvoiceFromRequest constructor to build an Invoice
from a corresponding request. This copies all non-signature fields from the
request (including unknown signed-range TLVs via the decodedTLVs sidecar)
and mirrors invreq_amount into invoice_amount.
2026-07-09 06:03:25 +00:00
bitromortac
732b0413c4
bolt12: inject feature-bit catalogues into Offer and InvoiceRequest validators
Inject known feature-bit catalogues into the read-side validators to enable
correct must-understand capability checks, and remove write-side feature
enforcement entirely.

Whether a feature bit is "unknown" is a runtime property of the reading node,
not of the wire format or pure codec.
2026-07-09 06:03:25 +00:00
bitromortac
e548d62dd2
bolt12: add TUint32 and invoice subtypes
Add the truncated uint32 (tu32) TLV type used by invoice_relative_expiry
and the dynamic invoice subtypes BlindedPayInfo and FallbackAddress,
along with their encode/decode helpers and round-trip tests.

These primitives are the building blocks for the BOLT 12 Invoice message
struct that follows. Isolating them keeps that codec commit focused on
the message shape rather than its component records.
2026-07-09 06:03:25 +00:00
Olaoluwa Osuntokun
d1ea8687c0 ci: split issue dedupe into find and post jobs
In this commit, we give the issue dedupe workflow the same shape: one job
finds the duplicate candidates, another posts the comment. The find job
records the candidate issue numbers to a file, and the post job hands
those numbers to comment-on-duplicates.sh, which already validates each
number and renders the comment from a fixed template.

Keeping detection and posting apart mirrors how the script is already
factored, so the post job ends up a thin wrapper over it. We also drop the
unused id-token permission and turn off checkout credential persistence
while we're in here.
2026-07-07 21:24:55 -07:00
Olaoluwa Osuntokun
05c625275e ci: split PR severity workflow into classify and apply jobs
In this commit, we separate the two concerns in the PR severity workflow:
working out the severity, and applying it. The classify job inspects the
PR and records its verdict (the severity level, whether to comment, and
the comment body) to a few files. A second apply job reads those files
and does the mechanical work of setting the label and posting the comment.

Pulling the classification apart from the application keeps each job doing
one thing and makes the flow easier to follow. The apply job takes the
severity the classifier picked and checks it against the known set before
touching a label, and posts the comment from a file via --body-file so the
body is handled as plain data. We also turn off checkout credential
persistence, since neither job needs a git credential on disk.
2026-07-07 21:24:55 -07:00
saubyk
447703a47c
github: bump gateway code-review to v0.5.0
Update the gateway-action pin and runtime_ref to the v0.5.0 release
commits, and extend the shim for the new inline-command support: a
pull_request_review_comment trigger plus comment_in_reply_to input so
/gateway dismiss, promote, and explain work as replies on a finding's
inline thread. Same fork-PR safety profile as issue_comment — comment
events receive no secrets on fork PRs.

Runtime highlights in v0.5.0: /gateway promote (file a finding as an
issue and dismiss it), batch dismiss, gateway-approved label with
stale-approval retraction, and one review comment per run with a
verdict-first body.
2026-07-07 16:34:37 -07:00
Olaoluwa Osuntokun
31168557c3
Merge pull request #10809 from ziggie1984/chanstate-openchannel-consumers
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
chanstate: use open channel from consumers
2026-07-07 14:47:07 -07:00
ziggie
489a6dabd9
chanstate: match active htlcs by identity
ActiveHtlcs previously matched HTLCs across the local and remote
commitment snapshots by hashing the onion blob. The onion blob is
routing payload data and can be duplicated by buggy or malicious
senders, so it is not a reliable key for identifying the same HTLC on
both commitments.

Match on the HTLC's channel identity instead: the channel-level HTLC
index combined with the direction of the offer uniquely identifies an
offered HTLC within the channel state. A test is added to lock in the
new matching behavior.
2026-07-07 09:07:54 -03:00
ziggie
04b7486fde
chanstate: complete open channel copy
The Copy method omitted the CloseConfirmationHeight and Db fields when
cloning an OpenChannel, so the returned copy silently diverged from the
original. Copy both fields over so the clone is a faithful copy, which
consumers that operate on channel copies rely on.
2026-07-07 09:00:47 -03:00
ziggie
a4bec71b54
htlcswitch: use channel state open channel
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.
2026-07-07 07:18:59 -03:00
ziggie
a459bc4672
peer: use channel state open channel
Update peer channel loading, validation, and test helpers to use
chanstate.OpenChannel directly.

The peer package still depends on channeldb for store-level errors and
helpers, but no longer needs the OpenChannel alias in its public
channel-state boundary.
2026-07-07 07:18:59 -03:00
ziggie
fa31c56464
lnwallet: use channel state open channel
Update lnwallet channel, reservation, wallet, and test helpers to use
chanstate.OpenChannel directly.

The wallet package still imports channeldb for database APIs and other
channel-state aliases, but the OpenChannel type boundary now points at
the package that owns the type.
2026-07-07 07:18:59 -03:00
ziggie
e0b869135c
contractcourt: use channel state open channel
Update contractcourt channel and resolver state boundaries to use
chanstate.OpenChannel instead of the channeldb alias.

This keeps the contract resolution package depending on channel state
data through the package that now owns the type, while leaving
channeldb references for store and error types that still belong there.
2026-07-07 07:18:59 -03:00
ziggie
05bea1dd69
chanrestore: use channel state open channel
Build restored channel shells with chanstate.OpenChannel instead of the
channeldb alias.

The restored shell is channel state data, so this keeps the constructor
aligned with the package that now owns the type.
2026-07-07 07:18:59 -03:00
ziggie
4a34503ca5
rpcserver: use channel state open channel
Update RPC helpers that format open channel data to accept
chanstate.OpenChannel directly.

These helpers only inspect channel state and do not need to name the
channeldb OpenChannel alias.
2026-07-07 07:18:59 -03:00
ziggie
8e709678d9
server: use channel state open channel
Update server callback wiring to use chanstate.OpenChannel at the
funding manager boundary.

This follows the funding package change and removes another
consumer-facing dependency on the channeldb OpenChannel alias.
2026-07-07 07:18:58 -03:00
ziggie
03daaad4fc
funding: use channel state open channel
Update the funding manager callback and helper signatures to depend on
the chanstate OpenChannel type instead of the channeldb alias.

The funding manager already receives channel persistence through the
chanstate Store interface, so this keeps its open-channel boundary
aligned with the backend-independent package.
2026-07-07 07:18:58 -03:00
ziggie
da94a411c8
chanstate: use channel types in link config
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.
2026-07-07 07:18:58 -03:00
ziggie
4bb78770ce
chanstate: use channel types in watchtower
Move watchtower blob and client channel-type boundaries to chanstate.

The wtclient manager still imports channeldb for closed-channel lookup
errors, but the channel type and close-summary payloads now use the
channel state package directly.
2026-07-07 07:18:58 -03:00
ziggie
7bd2447757
chanstate: use channel type in gossiper
Move the gossiper channel lookup callback to chanstate.OpenChannel.

Discovery still depends on channeldb for waiting-proof persistence.
This commit only removes the channeldb compatibility alias from the
channel state lookup boundary.
2026-07-07 07:18:58 -03:00
ziggie
a546c7591d
chanstate: use channel types in local chans
Move the local channel manager fetch boundary to chanstate types.

The manager still imports channeldb for the concrete not-found error,
but channel state values and config constraints now use the chanstate
package directly.
2026-07-07 07:18:58 -03:00
ziggie
3724915820
chanstate: use channel types in switch config
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.
2026-07-07 07:18:57 -03:00
ziggie
8a215d9e2a
chanstate: use channel type in wallet rpc
Move the waiting-close channel helper to chanstate.OpenChannel.

The helper consumes channel state returned by the store interface, so
it should not spell the channeldb compatibility alias. Other database
errors and APIs in the wallet RPC server remain on channeldb.
2026-07-07 07:18:57 -03:00
ziggie
897f2ce54c
chanstate: use channel type in backup notifier
Move the root backup notifier adapter to chanstate.OpenChannel.

The adapter still depends on channeldb for address sourcing and close
type handling, but the new-channel payload it forwards into chanbackup
now matches the chanstate-owned backup interfaces.
2026-07-07 07:18:57 -03:00
ziggie
dac8a8fe53
chanstate: use channel types in backups
Move static channel backup construction to chanstate channel types.

The package still imports channeldb where it uses real database
concerns, including address sourcing and duplicate-channel recovery.
The backup payload and live-channel source boundaries no longer depend
on the channeldb compatibility aliases.
2026-07-07 07:18:57 -03:00
ziggie
c58d51d593
chanstate: use channel type in lnpeer
Move lnpeer.NewChannel to embed chanstate.OpenChannel.

This keeps the peer-facing channel event type independent of the
channeldb compatibility alias while preserving the existing embedded
OpenChannel field shape for callers.
2026-07-07 07:18:57 -03:00
ziggie
cd0383450a
chanstate: use channel type in status manager
Move the channel status manager working set to chanstate.OpenChannel.

The DB interface already returns channelstate channel values, so this
removes another channeldb compatibility alias from the consumer path
while keeping the graph and announcement behavior unchanged.
2026-07-07 07:18:57 -03:00
ziggie
471c9b696f
chanstate: use htlc type in beacon
Move the witness subscription HTLC parameter to chanstate.HTLC.

The beacon still depends on channeldb for witness-cache errors, but it
no longer needs the channel DB compatibility alias for HTLC payloads.
2026-07-07 07:18:56 -03:00
ziggie
19e656b2bb
chanstate: use channel types in fitness
Move channel event payloads in chanfitness to chanstate types.

The event store still uses channeldb for flap-count persistence and
related errors, but open-channel and close-summary values now come from
the channel-state package.
2026-07-07 07:18:56 -03:00
ziggie
a3fdcba511
chanstate: use channel types in interfaces
Move small interface-only consumers to chanstate.OpenChannel.

These packages only expose open-channel values through callback or
store interfaces, so they can depend on the channel-state package
without pulling in channeldb compatibility aliases.
2026-07-07 07:18:56 -03:00
ziggie
fea9c171ee
chanstate: use channel types in consumers
Move channelnotifier and invoice hop-hint code to the chanstate channel
types.

These consumers already depend on the chanstate store interfaces, so
they no longer need to refer to the channeldb compatibility aliases for
OpenChannel and ChannelCloseSummary.
2026-07-07 07:18:56 -03:00
Olaoluwa Osuntokun
530301c19b
Merge pull request #10808 from ziggie1984/chanstate-openchannel-refactor-clean
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
chanstate: move OpenChannel behind store interface
2026-07-06 19:09:39 -07:00
ziggie
c5aabdf8c6
channeldb: document legacy fwd packager interface 2026-07-06 15:51:20 -03:00
ziggie
b720fd5cd5
chanstate: fix htlc copy
Copy all HTLC fields when cloning channel commitment state.

The old copy method only copied a subset of scalar fields and copied
into nil slices for Signature and ExtraData. Allocate those slices and
deep-copy custom record values so snapshots and channel copies retain
complete HTLC metadata.
2026-07-06 15:51:20 -03:00
ziggie
68ad283d3e
chanstate: remove store generics
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.
2026-07-06 15:51:20 -03:00
ziggie
07ede43ad1
chanstate: split commitment store interface 2026-07-06 15:50:54 -03:00
ziggie
3adc0133d0
chanstate: move open channel type
Move OpenChannel and its backend-neutral receiver methods into the
chanstate package.

channeldb now keeps a compatibility alias while retaining the KV store
implementation and serialization helpers. Tests that used private
channel status fields now use store-facing accessors.
2026-07-06 15:50:53 -03:00
ziggie
6eef3ad6b7
channeldb: add store status check
Add a transitional non-locking status predicate for channeldb store
code and use it from KV serialization helpers.

This avoids calling an unexported OpenChannel helper from channeldb
after the type moves into chanstate.
2026-07-06 15:50:53 -03:00
ziggie
c065aa9136
chanstate: move taproot channel helpers
Move the backend-neutral taproot shachain and verification nonce
helpers into chanstate with the thaw-height threshold they support.

Leave channeldb aliases for existing callers while OpenChannel and its
receiver methods are moved across the package boundary.
2026-07-06 15:50:53 -03:00
ziggie
aeab363a57
chanstate: move channel snapshot type
Move the backend-neutral ChannelSnapshot value type into chanstate and
leave channeldb with a compatibility alias.

This keeps the future OpenChannel Snapshot receiver close to its return
type without changing existing channeldb callers.
2026-07-06 15:50:53 -03:00
ziggie
0229e0ada3
channeldb: derive channel packagers
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.
2026-07-06 15:50:53 -03:00
ziggie
4fbb51b5ef
channeldb: add channel store accessors
Add transitional OpenChannel accessors for the channel status and
confirmed SCID fields used by KV store code.

These helpers keep the fields private while allowing channeldb backend
code to continue hydrating and serializing channel state after
OpenChannel moves to chanstate.
2026-07-06 15:50:52 -03:00
ziggie
a82ea84a20
channeldb: split out channel kv helpers
Convert the KV-only OpenChannel helpers for TLV aux data and
borked-state lookup into package-level channeldb helpers.

This keeps serialization and bucket inspection code tied to the KV
backend while leaving the OpenChannel receiver set closer to the future
chanstate type.
2026-07-06 15:50:52 -03:00
ziggie
40234f3c88
channeldb: store channel state by interface
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.
2026-07-06 15:50:52 -03:00
ziggie
e28122464e
channeldb: move revocation tail helper
Keep the revocation-log tail-height helper on ChannelStateDB instead of
the OpenChannel receiver.

The helper is only used by channeldb tests, so it should not become
part of the backend-independent chanstate store contract. The tests now
call the concrete helper directly.
2026-07-06 15:50:52 -03:00
ziggie
1bcd4d654e
chanstate: add previous state lookup
Add FindPreviousState to the chanstate commitment store subinterface now that
RevocationLog is a chanstate value type.

This extends the store contract without changing runtime behavior. The
existing ChannelStateDB method already satisfies the new method.
2026-07-06 15:50:52 -03:00
ziggie
dbc9a70134
chanstate: move revocation log types
Move the revocation-log value types and TLV serialization helpers into
chanstate.

Leave channeldb aliases and wrapper functions for the existing KV
persistence code and tests. Bucket keys, errors, and transaction
helpers stay in channeldb, so this commit only moves backend-neutral
state data.
2026-07-06 15:50:52 -03:00
ziggie
3e1aab02c9
channeldb: move revocation log reads
Move the remaining OpenChannel revocation-log KV reads onto
ChannelStateDB.

This keeps FindPreviousState and the unit-test tail-height helper as
OpenChannel wrappers. It removes direct backend access from the
receiver methods while leaving RevocationLog in channeldb for now.
2026-07-06 15:50:51 -03:00
ziggie
4a9ae1c526
chanstate: add commitment read stores
Add commitment-height, latest-commitment, and remote revocation store
lookups to the chanstate commitment store subinterface.

Move the existing OpenChannel KV view transaction bodies onto
ChannelStateDB. This leaves the receivers as store-call wrappers while
keeping the persisted format and read behavior unchanged.
2026-07-06 15:50:51 -03:00
ziggie
61f26df211
chanstate: add forwarding package store
Add a forwarding-package store subinterface to chanstate.Store.

Move the existing OpenChannel forwarding-package KV transaction bodies
onto ChannelStateDB. The OpenChannel receivers keep their locking
behavior and delegate package loading, acking, filtering, and removal
through the store.
2026-07-06 15:50:51 -03:00
ziggie
7029960d19
chanstate: add commit tail store
Add the commitment-tail advancement method to the chanstate commitment
store subinterface.

Move the existing AdvanceCommitChainTail KV transaction body onto
ChannelStateDB. The OpenChannel receiver now keeps locking and restored
channel checks before delegating persistence through the store.
2026-07-06 15:50:51 -03:00
ziggie
71864a32ec
chanstate: move forwarding package types
Move FwdState, PkgFilter, and FwdPkg into chanstate with their existing
comments and helper methods.

Leave channeldb aliases for the moved value types and constructors so
current callers keep compiling. The KV forwarding package persistence
code stays in channeldb.
2026-07-06 15:50:51 -03:00
ziggie
be2d0e5e60
chanstate: add revocation insert store
Add the next-revocation persistence method to the chanstate commitment
store subinterface.

Move the existing OpenChannel KV update body onto ChannelStateDB. The
OpenChannel receiver keeps the external locking behavior and delegates
persistence through the store interface.
2026-07-06 15:50:50 -03:00
ziggie
349567c772
chanstate: add commit lookup store
Add read-side commitment lookup methods to the chanstate commitment
store subinterface.

Move the existing OpenChannel KV view transaction bodies onto
ChannelStateDB. Leave the OpenChannel receivers as store-call wrappers.
This removes three more direct backend references from the receiver
code without changing the persisted data format.
2026-07-06 15:50:50 -03:00
ziggie
06308e1dc9
chanstate: add remote commit chain store
Add the remote commitment-chain append method to the chanstate
commitment store subinterface.

Move the existing KV transaction body onto ChannelStateDB and have the
OpenChannel receiver call through the store. This removes another
direct backend dependency from OpenChannel while keeping KV persistence
code in channeldb.
2026-07-06 15:50:50 -03:00
ziggie
9dfb8c1e13
chanstate: move commitment diff types
Move CommitDiff and its forwarding reference types into chanstate. This
lets the next commitment store subinterface name pending remote commitment
state without importing channeldb.

Keep forwarding package persistence and commit-diff serialization in
channeldb for now. The aliases preserve existing call sites while the
KV backend code remains in place.
2026-07-06 15:50:50 -03:00
ziggie
68ada20b9c
chanstate: add commitment store subinterface
Add a commitment-focused store subinterface for updating local channel
commitment state. This lets OpenChannel call through the chanstate
store contract instead of reaching directly into the KV backend.

Keep the existing KV transaction body on ChannelStateDB for now. The
receiver still owns locking and in-memory state updates while the store
method owns persistence.
2026-07-06 15:50:50 -03:00
ziggie
ed0bda738e
chanstate: move log update type
Move LogUpdate into chanstate so commitment store interfaces can refer
to pending update state without importing channeldb.

Keep the log-update serialization helpers in channeldb. Those helpers
remain part of the existing KV disk format and can move with the KV
backend implementation later.
2026-07-06 15:50:50 -03:00
ziggie
43edd57d23
chanstate: move commitment value types
Move ChannelCommitment and HTLC into chanstate so upcoming store subinterfaces
can name commitment state without importing channeldb.

Leave the KV serialization helpers in channeldb and keep aliases for
existing call sites. This preserves the current disk format and keeps
backend-specific persistence code out of chanstate for now.
2026-07-06 15:50:49 -03:00
ziggie
219987c99c
chanstate: add pending channel setup store
Add pending-channel setup to the chanstate lifecycle store subinterface. This
covers the path that writes a new pending channel and records the
funding broadcast height.

Move the OpenChannel receiver to call through ChannelStateDB and pass
the backend explicitly into the channeldb sync helper. This keeps the
link-node persistence detail in channeldb while removing another direct
backend reference from OpenChannel.
2026-07-06 15:50:49 -03:00
ziggie
0f96fb5ac5
chanstate: add open channel close stores
Add shutdown and close-transaction subinterfaces to the chanstate Store
contract. These cover persisted shutdown info plus stored unilateral
and cooperative closing transactions.

Implement the subinterfaces on ChannelStateDB with the existing KV code and
update OpenChannel receivers to call through the store methods. The
backend-specific key selection remains private to channeldb.
2026-07-06 15:50:49 -03:00
ziggie
4f7c21bd87
chanstate: add open channel status store
Add a status subinterface to the chanstate Store contract for status bit
updates and data-loss commit point handling. Implement the subinterface on
ChannelStateDB using the existing persistence code.

Update the matching OpenChannel receivers to call through the store
methods. The broadcast path still uses a private channeldb helper until
its closing-transaction subinterface is introduced in a later commit.
2026-07-06 15:50:49 -03:00
ziggie
849d097625
chanstate: add open channel lifecycle store
Add a lifecycle subinterface to the chanstate Store contract for refresh,
confirmation, open-state, and SCID mutations. Implement the subinterface on
ChannelStateDB using the existing KV persistence code.

Update the matching OpenChannel receivers to call through the store
methods instead of reaching into the ChannelStateDB backend directly.
Also convert fullSync into a channeldb helper so that KV-specific code
is no longer an OpenChannel receiver.
2026-07-06 15:50:49 -03:00
ziggie
444e80c0d9
chanstate: move shutdown metadata
Move the ShutdownInfo state type, constructor, and closer helper into
chanstate. The type describes channel shutdown state and is not tied to
the concrete KV backend.

Keep the TLV encode and decode helpers in channeldb for now, since
those functions describe the current persisted format. The channeldb
constructor remains as a compatibility wrapper.
2026-07-06 15:50:49 -03:00
ziggie
c96ba05c70
chanstate: move open channel errors
Move the OpenChannel error definitions into chanstate and leave
channeldb aliases for existing callers. These errors describe channel
state behavior rather than a concrete KV bucket layout.

Keeping the aliases preserves the public channeldb API while later
commits move more OpenChannel state and receiver logic toward
chanstate.
2026-07-06 15:50:48 -03:00
ziggie
fb0bf755d5
chanstate: move channel type flags
Move ChannelType and its flag helpers into chanstate while leaving
compatibility aliases in channeldb. This is a backend-neutral value
type and does not require moving any KV serialization logic.

Keep the full type documentation with the moved chanstate definition.
The channeldb aliases preserve the existing public surface while later
commits continue moving OpenChannel state out of the KV package.
2026-07-06 15:50:48 -03:00
ziggie
f53d615c1b
chanstate: make store channel types generic
Move the small value types referenced by chanstate.Store out of
channeldb. This includes ChannelConfig, ChannelStatus,
ChannelCloseSummary, ChannelShell, ChanCount, and FinalHtlcInfo. Leave
aliases in channeldb so existing callers keep compiling while the
backend still lives there.

Parameterize the Store subinterfaces over the channel type and instantiate
current callers with *channeldb.OpenChannel. This removes the chanstate
-> channeldb import edge without moving OpenChannel yet, keeping the
first step reviewable and backend-neutral.
2026-07-06 15:50:41 -03:00
Yong
40c64f9db3
Merge pull request #10900 from ellemouton/walletkit-submitpackage
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
lnwallet+walletrpc: add WalletKit.SubmitPackage for v3 CPFP package relay
2026-07-01 05:42:09 +08:00
Elle Mouton
68c47407d7
docs: add release note for WalletKit.SubmitPackage
Document the new WalletKit.SubmitPackage RPC and the lncli wallet
submitpackage command in the 0.22.0 release notes.
2026-06-30 11:07:52 -07:00
Elle Mouton
47661cea86
itest: add SubmitPackage integration test
Add an integration test that exercises WalletKit.SubmitPackage: it builds
a zero-fee v3 (TRUC) parent that a standalone broadcast would reject,
pairs it with a fee-paying v3 CPFP child, and asserts the package is
accepted. A zero-fee transaction can only enter the mempool via package
evaluation, so this proves the CPFP package path end to end.

submitpackage is a bitcoind RPC, so the test skips on the btcd and
neutrino backends. Also adds the SubmitPackage wrapper to the
integration-test RPC harness.
2026-06-30 11:07:52 -07:00
Elle Mouton
ea88a26870
lncli: add wallet submitpackage command
Add a `wallet submitpackage` command that takes one or more hex-encoded
raw transactions (topologically sorted, parents first and the child
last) and an optional --max_fee_rate, and submits them as a package via
the WalletKit.SubmitPackage RPC.
2026-06-30 11:07:52 -07:00
Elle Mouton
f55c0565d0
lnwallet+walletrpc: add SubmitPackage for v3 CPFP package relay
Add SubmitPackage to the lnwallet.WalletController interface and a new
WalletKit.SubmitPackage RPC, so a client of lnd can relay a package of
related transactions (parents first, child last) through lnd's own chain
connection. This lets a zero-fee v3/TRUC parent be accepted via its
fee-paying CPFP child without the caller needing a separate connection to
the chain backend.

BtcWallet.SubmitPackage forwards to the chain backend's submitpackage for
bitcoind/btcd, and broadcasts each transaction individually for neutrino
(no mempool; relies on the peer's 1p1c package relay). The WalletKit
handler maps the proto request/response to the btcjson result and is
gated by the onchain:write macaroon permission. Mock controllers and the
no-chain backend gain trivial implementations.
2026-06-30 11:07:52 -07:00
ziggieXXX
c4a67b68b7
Merge pull request #10832 from bitromortac/2604-bolt12-1b
bolt12: add `InvoiceRequest` codec and structural validators
2026-06-30 14:45:53 -03:00
bitromortac
e9a2cb43d4
docs: update release notes
Add a BOLT 12 release note for the invoice_request codec, completing the
offer/invoice_request entries for the bolt12 package in 0.22.0.
2026-06-30 18:20:46 +02:00
bitromortac
8b4587b017
bolt12: validate InvoiceRequest per BOLT 12 reader/writer requirements
ValidateInvoiceRequestRead and ValidateInvoiceRequestWrite enforce the
structural BOLT 12 requirements an invoice request can be checked
against on its own. The reader validates incoming requests. The writer
catches out-of-range types in decoded-then-mutated requests before they
leave the local boundary. Type 240 carries the signature and sits
outside the allowed range by spec design. Both validators skip it
during the range scan.

Two reader MUSTs are deferred. Schnorr signature verification against
the merkle root keyed by invreq_payer_id lands with the Invoice
message, where the merkle and signing primitives are shared. Offer
cross-validation requires an Offer reference the structural validator
does not carry, and lands in the bolt12handler layer where both the
request and the stored Offer are in scope.
2026-06-30 18:20:43 +02:00
bitromortac
338e4bda90
bolt12: add ErrNilPublicKey and type the offer_issuer_id guard
The offer writer rejected a present-but-nil offer_issuer_id with an
ad-hoc error. Introduce a typed ErrNilPublicKey sentinel and use it here
so the rejection is recoverable by callers and reusable by the
invoice_request writer, which guards the same hazard for its own pubkey
fields.
2026-06-30 18:20:03 +02:00
bitromortac
c419ba5671
bolt12: add NewInvoiceRequestFromOffer
This ensures we copy all fields when replying to an offer.
2026-06-30 18:20:00 +02:00
Yong
b2d0932d67
Merge pull request #10940 from gijswijs/add-gijswijs-sig
Add public key for gijswijs
2026-06-30 23:19:28 +08:00
Yong
d4f8752182
Merge pull request #10930 from yyforyongyu/cleanup-tlv-tor-tags
mod: use tagged tlv and tor versions
2026-06-30 21:27:23 +08:00
Gijs van Dam
74102ae633
scripts/keys: add pub key for gijswijs 2026-06-29 21:40:17 +02:00
ziggieXXX
cbe77749cd
Merge pull request #10895 from ziggie1984/fix-onchain-interceptor-held-htlc
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
htlcswitch: separate onchain and offchain intercpeted HTLCs cleanly
2026-06-26 13:59:31 -03:00
yyforyongyu
0b30a4fa1b
mod: use tagged tlv and tor modules 2026-06-26 23:40:57 +08:00
ziggie
9c5f32a2ec
docs: add v0.21.1 release note 2026-06-26 12:23:07 -03:00
ziggie
8909c2fbf5
routerrpc: add clarifying docs for the intercepted forward
routerrpc: document on-chain interceptor responses
2026-06-26 12:23:07 -03:00
ziggie
98da7b4a56
witnessbeacon: avoid interceptor deadlock
Release the preimage beacon lock before invoking the on-chain
interceptor. The interceptor path can block on the htlcswitch event
loop, while resolution of another held on-chain HTLC can call back
into the beacon to add a preimage.

If interceptor delivery fails after the subscriber was registered,
cancel the subscription before returning the error.

On-chain held entries are replay handles for the interceptor while
contractcourt waits for a preimage or on-chain expiry. Once the resolver
tears down, keeping the handle until the refund timeout can replay a stale
HTLC to a reconnecting interceptor.

Thread a dedicated cleanup signal from the witness subscription cancel path
back through the interceptable switch event loop. The held set only removes
on-chain entries for that signal, leaving off-chain entries under the link
flow lifecycle.
2026-06-26 12:23:07 -03:00
ziggie
eb1193f80b
htlcswitch: track held HTLC source
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.
2026-06-26 12:23:06 -03:00
ziggie
9b31ba83ef
itest: cover on-chain interceptor settlement
Add coverage for held forwards that move on chain after the
incoming channel force closes.

The restart case exercises the path where Bob loses the in-memory
held set and contractcourt re-offers the HTLC through the witness
beacon. The no-restart case keeps the original off-chain hold and
proves that settlement must still reach the on-chain resolver.
2026-06-26 12:08:59 -03:00
Yong
0cad457be1
Merge pull request #10927 from ziggie1984/align-final-hop-cltv-handling
htlcswitch+invoices: tighten final-hop CLTV validation
2026-06-26 16:59:29 +08:00
bitromortac
6bb9c7e8cf
bolt12: add InvoiceRequest message struct and TLV codec
The InvoiceRequest is the BOLT 12 message that links a payer to an
offer: it mirrors the offer's fields so the issuer can stay stateless,
and adds the payer-specific fields and Schnorr signature that prove the
request.

It implements lnwire.PureTLVMessage so it round-trips through the shared
TLV codec.
2026-06-26 07:42:04 +02:00
Suheb
5e1e827f01
github: bump gateway code-review to v0.4.4 (#10926)
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
Bump the gateway-action pin to v0.4.4 (abe7cf8) and the runtime_ref to
gateway v0.4.4 (20675fc), and drop the hardcoded installation_id. As of
v0.4.4 the runtime resolves the App installation covering this repo from
app_id/private_key, so a static (and easily wrong-org) id is no longer
needed.
2026-06-26 01:57:46 +00:00
ziggie
bffae65a60
docs: update release notes 2026-06-25 19:06:26 -03:00
ziggie
4518bc7276
htlcswitch: fix outgoing cltv spelling
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.
2026-06-25 19:06:26 -03:00
ziggie
03ca45a282
contractcourt: align final htlc handling in contest resolver
Mirror the link's final-hop HTLC checks in the incoming contest resolver so
the off-chain link path and on-chain resolver use the same final-hop handling.

Use MaxFinalCltvDelta directly in contractcourt to match invoice creation and
link processing.

Preserve the link's custom HTLC behavior by leaving amount checks to auxiliary
traffic shapers when custom HTLC handling applies.
2026-06-25 19:06:25 -03:00
ziggie
b8e861fe6b
config: check cltv expiry policy range
Check configured and advertised forwarding CLTV deltas against max-cltv-expiry
so local configuration and advertised channel policy stay within the same
supported range.
2026-06-25 18:56:39 -03:00
ziggie
c5992d381f
htlcswitch+invoices: align final-hop CLTV handling
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.
2026-06-25 16:28:55 -03:00
ziggieXXX
0dbe2b1029
Merge pull request #10913 from guggero/btcd-v2-modules
Update everything to btcd v2 modules
2026-06-24 23:48:37 -03:00
ziggieXXX
cea51aa075
Merge pull request #10923 from erickcestari/remove-discovery-release-notes-0.22
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
docs: remove release note for DNS fallback panic of 0.22.0
2026-06-24 19:20:05 -03:00
Erick Cestari
1ab34c4eff
docs: add release note for DNS fallback SRV lookup panic fix 0.21.1 2026-06-24 17:04:07 -03:00
Erick Cestari
48ba0edf10
docs: remove release note for DNS fallback panic of 0.22.0 2026-06-24 17:03:51 -03:00
Elle Mouton
3912428958
multi: fix lint findings from btcd v2 migration
The btcd v2 module migration re-touched several lines that the line-length
linter then flagged, and left one error return unchecked. Wrap the
over-length lines in input/test_utils.go and zpay32 (the address-decode
helpers and test fixtures whose btcutil->address rename lengthened them),
and check the LoadTxFilter error return in routing/chainview/btcd.go.
2026-06-24 11:57:10 -07:00
Oli
8047149c6a
multi: upgrade to btcd v2 modules
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).
2026-06-24 10:58:36 -07:00
Yong
c6768ca4ff
Merge pull request #10914 from erickcestari/fix-panic-discovery
discovery: fix panic in DNS fallback SRV lookup
2026-06-24 23:28:00 +08:00
Erick Cestari
2ee49698af
docs: add release note for DNS fallback SRV lookup panic fix 2026-06-23 14:52:25 -03:00
Erick Cestari
2a3642c691
discovery: fix panic in DNS fallback SRV lookup
The fallback SRV lookup type-asserted each DNS Answer record to *dns.SRV
unconditionally. If the response contains a non-SRV record (e.g. an A or
CNAME), the type assertion panics and crashes the daemon. Use the
comma-ok form to skip non-SRV records instead.

Also guard against an empty LookupHost result for the shim, which would
otherwise panic on an out-of-bounds index into addrs.

This is safe to discuss and fix in public. The bug is very unlikely to be
exploitable: triggering it requires either a DNS seeder to serve a
malformed response, or an on-path MITM injecting one (the fallback
response is unauthenticated). A malicious seeder already has far more
direct ways to disrupt a node, and a MITM attack is hard to mount, so the
panic does not meaningfully widen the attack surface.
2026-06-23 14:52:16 -03:00
Yong
b927e03a20
Merge pull request #10916 from starius/skip-del-onion-if-no-service-id
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
tor: skip onion cleanup before service creation
2026-06-23 19:28:16 +08:00
bitromortac
c5733c40f5
lnrpc: document reply_path verbatim passthrough on OnionMessageUpdate
Document that the introduction_node field in an OnionMessageUpdate's
reply_path is passed through verbatim from the wire, potentially
carrying either a 33-byte pubkey or a 9-byte sciddir form. Subscribers
wishing to reply must resolve sciddir forms against their local channel
graph.

The SubscribeOnionMessages bridge is refactored to use a new
marshallBlindedPath helper, ensuring a nil reply path remains nil in the
RPC response rather than being emitted as an empty struct.
2026-06-23 11:09:19 +02:00
Boris Nagaev
d7ee3429ce
tor: skip onion cleanup before service creation
Only send DEL_ONION if ADD_ONION completed successfully and the
controller has an active service ID to remove.

This avoids masking the original ADD_ONION failure with a secondary
empty DEL_ONION error during startup cleanup.
2026-06-22 19:00:18 -05:00
ziggieXXX
1e8b76a579
Merge pull request #10904 from yyforyongyu/rpc-panic-recovery
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
rpcperms: recover RPC handler panics
2026-06-22 20:43:32 -03:00
yyforyongyu
4bbfcab910
rpcperms: recover RPC handler panics 2026-06-23 01:37:15 +08:00
Suheb
2e6e61d308
github: add gateway code-review workflow (#10910)
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
* github: add gateway code-review workflow

Opt-in review bot invoked via /gateway review PR comments (maintainer-gated).
Thin shim onto the public lightninglabs/gateway-action (SHA-pinned to v0.4.3);
the review runtime stays private. Comment-commands only, so fork PRs never
spawn failing runs.

* github: address review on gateway workflow

- Gate the job on a /gateway command in the comment body so unrelated PR
  comments don't spin up no-op runners. Use contains() (not startsWith) since
  the runtime accepts the command at column 0 of any line, incl. multi-line.
- Pin runtime_ref to the gateway commit SHA so runtime upgrades require an lnd
  PR rather than a moved tag, matching the action SHA-pin.
2026-06-18 22:06:26 -07:00
Yong
918b94d0d6
Merge pull request #10907 from ziggie1984/fix-tor-v3-onion-tagged-module
Some checks failed
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
Vulnerability scan / Scan release binaries (push) Has been cancelled
build: update tor module to v1.1.7
2026-06-19 02:58:03 +08:00
ziggie
9b21d0a492
docs: add 0.21.1 release notes 2026-06-18 08:38:23 -03:00
ziggie
54b3308ab5
build: update tor module to v1.1.7 2026-06-18 08:32:06 -03:00
Yong
f771e14191
Merge pull request #10903 from ZZiigguurraatt/cache_golang
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
dev.Dockerfile: use cache mounts to speed up rebuilding
2026-06-17 21:31:12 +08:00
ZZiigguurraatt
20e651869c dev.Dockerfile: use cache mounts to speed up rebuilding 2026-06-16 15:28:20 -04:00
ziggieXXX
1f23a119d0
Merge pull request #10789 from bitromortac/2604-bolt12-1a
Some checks failed
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
Vulnerability scan / Scan release binaries (push) Has been cancelled
bolt12+lnwire: add codec foundation with Offer message
2026-06-15 22:10:31 -03:00
Yong
0b3e3aefe3
Merge pull request #10890 from erickcestari/fix-offline-peer-uptime
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
chanfitness: seed peer uptime from actual online state
2026-06-15 12:19:41 +08:00
Yong
a067468f0f
Merge pull request #10902 from guggero/btcd-v2-modules-for-submodules
tlv+tor: update btcd modules to v2
2026-06-15 10:29:59 +08:00
Erick Cestari
09f38dac36
docs: add release notes for offline peer uptime fix 2026-06-12 15:19:26 -03:00
Erick Cestari
4aefff0453
chanfitness: seed peer uptime from actual online state
Channels are loaded into the channel fitness store on startup regardless
of whether their peer is connected. When a peer monitor was first
created we unconditionally recorded an online event, which caused
offline peers to report 100% uptime in ListChannels.

Seed the initial event with the peer's actual connection state via a new
IsPeerOnline config callback so that uptime reflects real connectivity.
2026-06-12 15:19:24 -03:00
Oli
bdba8e0ddc
tor: use latest btcd 2026-06-12 16:08:10 +02:00
Oli
5a5349ed46
tlv: use btcd v2 submodules 2026-06-12 15:38:06 +02:00
ziggieXXX
1a6ecebcbf
Merge pull request #10894 from starius/timeout0default60
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
routerrpc: document default timeout for EstimateRouteFee probes
2026-06-10 20:14:26 -03:00
Yong
21a338512c
Merge pull request #10872 from erickcestari/enable-copyloopvar-linter
lint: reactivate several disabled linters
2026-06-11 05:23:47 +08:00
Boris Nagaev
dfeb678a49
routerrpc: document default timeout for EstimateRouteFee probes
The RouteFeeRequest.timeout field did not document its behavior when
unset or explicitly set to zero. This is easy to misread as "no
timeout", i.e. an unbounded, uncancellable probe, especially given the
adjacent note that canceling the context does not stop the payment
loop.

In practice the probe path runs through SendPaymentV2, which replaces a
zero timeout_seconds with DefaultPaymentTimeout (60 seconds) before
dispatching the probe. A zero or unset timeout therefore falls back to
the same 60 second default that SendPaymentRequest.timeout_seconds
already documents.

Mirror that wording on RouteFeeRequest.timeout so the zero-value
behavior is explicit, and update the generated gRPC stub and swagger
description to match. Documentation only; no behavior change.
2026-06-10 15:39:59 -05:00
Yong
afeb9e1211
Merge pull request #10880 from ziggie1984/ci/check-final-release-ots
ci: require signer OTS proof for final releases
2026-06-10 13:39:45 +08:00
ziggieXXX
cd201a0a65
ci: require OTS proofs for final releases 2026-06-09 21:17:57 -03:00
Yong
036d1e0a5f
Merge pull request #10879 from ziggie1984/bump-pgx-v5-5.9.2
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
build: bump pgx to v5.9.2
2026-06-10 00:15:50 +08:00
ziggieXXX
7b7f55715b
Merge pull request #10887 from ziggie1984/cleanup-stale-module-replaces
mod: clean up dependency replacements
2026-06-09 12:16:58 -03:00
ziggie
079b1610a7
kvdb: bump pgx to v5.9.2 2026-06-09 07:56:53 -03:00
ziggie
1da2c7869a
sqldb/v2: bump pgx to v5.9.2 2026-06-09 07:56:53 -03:00
ziggie
3aeeea8791
sqldb: bump pgx to v5.9.2 2026-06-09 07:56:52 -03:00
ziggie
d4bcd9026e
lnd: bump pgx to v5.9.2 2026-06-09 07:53:02 -03:00
ziggie
6837d1aadf
tools: bump prometheus client dependency
Bump the nested tools module's Prometheus client dependency to v1.23.2
and let MVS select the newer common, procfs, and client_model versions
required by that release.

This removes the old Prometheus common v0.4.1 graph edge and drops the
stale github.com/gogo/protobuf v1.1.1 go.mod checksum from tools/go.sum.
2026-06-09 07:34:14 -03:00
ziggie
80f98a5edb
kvdb: remove stale xz replace
Remove the nested kvdb module's github.com/ulikunitz/xz replace
directive.

The current kvdb module graph does not select github.com/ulikunitz/xz,
so the historical vulnerability workaround is no longer needed in this
nested go.mod.
2026-06-09 07:34:13 -03:00
ziggie
261372bca4
kvdb: remove redundant gogo protobuf replace
Remove the nested kvdb module's self-replace for
github.com/gogo/protobuf now that the module graph selects v1.3.2
directly.

The Prometheus dependency graph was already updated in the previous
commit, so go mod tidy does not retain the old github.com/gogo/protobuf
v1.1.1 go.mod checksum.
2026-06-09 07:33:13 -03:00
ziggie
316a2dfa2f
kvdb: bump prometheus client dependency
Bump the nested kvdb module's Prometheus client dependency to v1.23.2
and let MVS select the newer common, procfs, and client_model versions
required by that release.

This removes the old Prometheus common v0.4.1 graph edge that referenced
github.com/gogo/protobuf v1.1.1, matching the root module cleanup.
2026-06-09 07:19:03 -03:00
ziggie
3f3718caa0
mod: bump protobuf hex display fork
Update google.golang.org/protobuf to v1.36.11 and point the replace
directive at the matching lightninglabs/protobuf-go-hex-display
v1.36.11-hex-display tag.

This keeps the fork aligned with the latest upstream protobuf-go release
while preserving the UseHexForBytes option used by the CLI JSON marshal
and unmarshal paths.
2026-06-09 07:19:03 -03:00
ziggie
8b7ededaba
mod: bump prometheus client dependency
Bump the direct Prometheus client_golang dependency to v1.23.2 and let
MVS select the newer common, procfs, and client_model modules required
by that release.

The newer Prometheus graph no longer references
github.com/prometheus/common v0.4.1, which was the remaining path that
caused go mod tidy to retain the github.com/gogo/protobuf v1.1.1 go.mod
checksum after removing the redundant gogo/protobuf replace.
2026-06-09 07:19:03 -03:00
ziggie
eeaa717eb0
mod: remove stale xz replace
The github.com/ulikunitz/xz replacement was added for CVE-2021-29482
when an older embedded-postgres dependency chain pulled in the affected
module indirectly.

That module is no longer part of the selected dependency graph: go mod
why reports that the main module does not need it, and go list -m
reports it is not a known dependency. Keeping the replacement no longer
affects builds.
2026-06-09 07:19:03 -03:00
ziggie
ee7c48d2a2
mod: remove redundant gogo protobuf replace
The main module already requires github.com/gogo/protobuf v1.3.2, which
is the fixed and latest tagged version. Go's MVS continues to select
v1.3.2 without the self-replace, so the replace no longer changes the
effective dependency version.

Running go mod tidy records an older transitive go.mod checksum, but go
list still resolves github.com/gogo/protobuf to v1.3.2.
2026-06-09 07:19:02 -03:00
Olaoluwa Osuntokun
1d4c753bf5
Merge pull request #10875 from ziggie1984/bump-go-1.25.11-toolchain-1.26.4
build: bump Go language to 1.25.11 and toolchain to 1.26.4
2026-06-08 20:31:18 -05:00
ziggie
92e29a422e
build: bump Go versions 2026-06-08 17:02:58 -03:00
ziggieXXX
6b27461f00
Merge pull request #10791 from rloomba/rloomba/bump-runc
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
build: bump runc to v1.2.8
2026-06-06 15:11:54 -03:00
Ryan Loomba
28f8055822 build: bump runc to v1.2.8 2026-06-06 09:43:10 -07:00
Olaoluwa Osuntokun
4cc4bbc95f
Merge pull request #10877 from Roasbeef/bump-version-v0.21.99
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
build: bump master version to v0.21.99
2026-06-05 00:47:04 +02:00
Olaoluwa Osuntokun
c2941a96ca scripts: match upstream remote case-insensitively in tag-release.sh
In this commit, we fix the upstream remote detection in tag-release.sh so
it recognizes remotes whose URL uses a different case than the canonical
`lightningnetwork/lnd`. GitHub treats the org/repo path as
case-insensitive, but the awk match was case-sensitive, so a remote
pointing at `LightningNetwork/lnd` (a common spelling for `origin`) would
go undetected and the script would bail out with "no git remote points at
lightningnetwork/lnd" even though one clearly did.

We lower-case the URL with awk's `tolower()` before matching, which keeps
the pattern itself lower-case and stays portable across the BSD awk on
macOS and gawk in CI (unlike the gawk-only `IGNORECASE`).
2026-06-04 13:46:55 -07:00
Olaoluwa Osuntokun
dd39280b58 build: bump master version to v0.21.99
This reflects the fact that master is beyond the last major release and
will be a super set of anything in the v0.21.x series.
2026-06-04 13:42:15 -07:00
Erick Cestari
3f677e60bb
lint: enable bodyclose, rowserrcheck and sqlclosecheck linters 2026-06-03 16:46:23 -03:00
Erick Cestari
d680013b02
lint: enable wastedassign linter 2026-06-03 16:46:23 -03:00
Erick Cestari
f1b34b8168
lint: fix wastedassign issues in test files 2026-06-03 16:46:23 -03:00
Erick Cestari
d9355bbe0f
lint: fix wastedassign issues in non-test files 2026-06-03 15:32:46 -03:00
Erick Cestari
c324b518d2
lint: enable copyloopvar and remove redundant loop var copies in non-test files
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 enables the `copyloopvar` linter so these are caught
automatically and removes the existing redundant copies in non-test
files.
2026-06-03 15:32:46 -03:00
Erick Cestari
aca27e27f5
lint: remove redundant loop var copies in test files
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.
2026-06-03 15:32:45 -03:00
ziggieXXX
653b23ce6f
Merge pull request #10858 from yyforyongyu/benchmark-fetch-inflight-payments
Some checks are pending
CI / Run unit tests-1 (push) Waiting to run
CI / Run unit tests-2 (push) Waiting to run
CI / Run unit tests-3 (push) Waiting to run
CI / Run unit tests-4 (push) Waiting to run
CI / Run unit tests-5 (push) Waiting to run
CI / Run unit tests-6 (push) Waiting to run
CI / Run unit tests-7 (push) Waiting to run
CI / Run unit tests-8 (push) Waiting to run
CI / Run unit tests-9 (push) Waiting to run
CI / Run basic itests (push) Waiting to run
CI / Run basic itests-1 (push) Waiting to run
CI / Run basic itests-2 (push) Waiting to run
CI / Run basic itests-3 (push) Waiting to run
CI / Run basic itests-4 (push) Waiting to run
CI / Run itests (push) Waiting to run
CI / Run itests-1 (push) Waiting to run
CI / Run itests-2 (push) Waiting to run
CI / Run itests-3 (push) Waiting to run
CI / Run itests-4 (push) Waiting to run
CI / Run itests-5 (push) Waiting to run
CI / Run itests-6 (push) Waiting to run
CI / Run itests-7 (push) Waiting to run
CI / Run windows itest (push) Waiting to run
CI / Run macOS itest (push) Waiting to run
CI / Check pinned dependencies (push) Waiting to run
CI / Check pinned dependencies-1 (push) Waiting to run
CI / Check release notes updated (push) Waiting to run
CI / Backwards compatibility test (push) Waiting to run
CI / Cache Cleanup (push) Waiting to run
CI / Send coverage report (push) Blocked by required conditions
payments: Add `FetchInFlightPayments` Benchmarks
2026-06-03 15:00:58 -03:00
yyforyongyu
950440d592
paymentsdb: benchmark FetchInFlightPayments on Postgres 2026-06-03 21:43:56 +08:00
yyforyongyu
90a41046d8
paymentsdb: add FetchInFlightPayments benchmark 2026-06-03 21:43:42 +08:00
bitromortac
bb897714e0 docs: update release notes 2026-06-03 12:12:25 +02:00
bitromortac
0cf82818e8 bolt12: validate Offer per BOLT 12 reader/writer requirements
ValidateOfferRead and ValidateOfferWrite enforce the codec-side portion
of the BOLT 12 offer reader and writer requirements. Reader rules cover
TLV range, even-feature-bit rejection, chain mismatch, dependency rules
between offer_amount/description/currency, missing issuer identity,
zero-hop blinded paths, and offer expiry. Writer rules mirror the same
dependency and identity guards plus a defense-in-depth empty-
offer_chains rejection.

offer_currency is validated against the ISO 4217 registry via
golang.org/x/text/currency (now a direct dependency); offer_issuer_id is
verified to be an on-curve SEC1 compressed point on both read and write
paths. Encode invokes Validate so invalid bytes never reach the wire.
2026-06-03 12:12:23 +02:00
bitromortac
bc2089588c bolt12: add Offer message struct with TLV codec
The Offer struct models a long-lived, reusable BOLT 12 payment template.
It defines TLV fields as optional records and exposes Encode/DecodeOffer
for round-trip serialization. The struct implements
lnwire.PureTLVMessage; AllRecords filters the decoded TypeMap through
bolt12InUnsignedRange to derive any signed-range extras the encoder must
re-emit, keeping offer_id and the Merkle root stable across encoders
that understand a wider set of even/odd extensions.
2026-06-03 12:11:29 +02:00
bitromortac
65db8fee49 bolt12: add chains TLV subtype
Introduce the ChainsRecord subtype used by the offer_chains and
invoice_chains TLV fields. Decoding caps the count at maxOfferChains to
bound allocation.
2026-06-03 12:11:29 +02:00
bitromortac
771c34f2f5 multi: migrate OnionMessagePayload to lnwire.BlindedPath
Switch OnionMessagePayload.ReplyPath from *sphinx.BlindedPath to
*lnwire.BlindedPath. The reply-path TLV is now produced and consumed by
(*lnwire.BlindedPath).Record(), which honours the BOLT 4 sciddir_or_pubkey
introduction-node form. The legacy decoder gated on a 67-byte minimum
length and silently rejected reply paths whose introduction node used
the 9-byte sciddir variant.

The legacy replyPathRecord / replyPathSize / encodeReplyPath /
decodeReplyPath / blindedHopSize / encodeBlindedHop / decodeBlindedHop
helpers and the unused ErrNoHops sentinel are deleted.

Consumers update mechanically: routing/route's
OnionMessageBlindedPathToSphinxPath replyPath parameter, the
onionmessage.OnionMessageUpdate field, the rpcserver onion-message
subscription bridge, and the lnwire test utilities now use the lnwire
type directly. The new TestOnionMessagePayloadRoundTrip "sciddir intro
reply path" subtest pins the BOLT 4 spec fix.
2026-06-03 12:11:29 +02:00
bitromortac
9113a53337 lnwire: add bounded introNode BlindedPath codec
Introduce the canonical lnwire.BlindedPath / BlindedPaths codec with a
sealed IntroductionNode sum-type covering both the BOLT 4 pubkey and
sciddir variants. The codec gates every variable-length subfield against
an io.LimitedReader. It fails closed on the encoder side so invalid
input never hits the wire.

This commit is a pure addition: no existing caller changes. Subsequent
commits migrate OnionMessagePayload and the bolt12 message structs to
consume the new codec.
2026-06-03 12:11:29 +02:00
bitromortac
027a699bfe lnwire: add SetOptFromMap and AddOpt
This gives us easier optional tlv field handling, which we will use for
the following message definitions.
2026-06-03 12:11:29 +02:00
bitromortac
85531ab6c1 lnwire: generalize pure TLV signed-range filtering
Add UnsignedRangeFunc and the SerialiseFieldsToSignFn /
ExtraSignedFieldsFromTypeMapFn variants so callers with non-BOLT 7 v2
signed ranges (e.g. BOLT 12, which reserves only 240-1000) can plug in
their own predicate. The existing SerialiseFieldsToSign and
ExtraSignedFieldsFromTypeMap entry points keep their behaviour by
delegating to the Fn variants with InUnsignedRange.
2026-06-03 12:11:29 +02:00
Yong
3e39a4dbe8
Merge pull request #10862 from yyforyongyu/update-gomod
Some checks failed
CI / Run unit tests-1 (push) Has been cancelled
CI / Run unit tests-2 (push) Has been cancelled
CI / Run unit tests-3 (push) Has been cancelled
CI / Run unit tests-4 (push) Has been cancelled
CI / Run unit tests-5 (push) Has been cancelled
CI / Run unit tests-6 (push) Has been cancelled
CI / Run unit tests-7 (push) Has been cancelled
CI / Run unit tests-8 (push) Has been cancelled
CI / Run unit tests-9 (push) Has been cancelled
CI / Run basic itests (push) Has been cancelled
CI / Run basic itests-1 (push) Has been cancelled
CI / Run basic itests-2 (push) Has been cancelled
CI / Run basic itests-3 (push) Has been cancelled
CI / Run basic itests-4 (push) Has been cancelled
CI / Run itests (push) Has been cancelled
CI / Run itests-1 (push) Has been cancelled
CI / Run itests-2 (push) Has been cancelled
CI / Run itests-3 (push) Has been cancelled
CI / Run itests-4 (push) Has been cancelled
CI / Run itests-5 (push) Has been cancelled
CI / Run itests-6 (push) Has been cancelled
CI / Run itests-7 (push) Has been cancelled
CI / Run windows itest (push) Has been cancelled
CI / Run macOS itest (push) Has been cancelled
CI / Check pinned dependencies (push) Has been cancelled
CI / Check pinned dependencies-1 (push) Has been cancelled
CI / Check release notes updated (push) Has been cancelled
CI / Backwards compatibility test (push) Has been cancelled
CI / Cache Cleanup (push) Has been cancelled
CI / Send coverage report (push) Has been cancelled
gomod: update `kvdb` and `btcwallet`
2026-06-01 18:44:38 +08:00
yyforyongyu
a2e3ecbff9
mod: bump sqldb to v1.0.13
Bump only the require version for github.com/lightningnetwork/lnd/sqldb
from v1.0.11 to v1.0.13. The local replace directive (=> ./sqldb) is
kept on purpose, so lnd itself continues to build against the in-tree
sqldb package on the release branch -- no behavioral change here.

The important part is downstream module resolution. Go does not
propagate replace directives from dependencies, so downstream consumers
of lnd only see the require version. Before this bump, they could
resolve an older tagged sqldb that did not contain the current
release-branch sqlc types and migrations, causing build failures.

The new sqldb/v1.0.13 tag was created from the v0.21 release-branch
commit that contains the same sqldb code lnd currently uses via the
local replace. Downstream consumers now fetch a tagged module matching
the release branch; the replace will be dropped in a follow-up cleanup.
2026-05-29 01:14:24 +08:00
yyforyongyu
5b45ac81a1
mod: bump btcwallet to v0.16.18
Replace the pseudo-version pin on commit 70a94ea39e9c with the freshly
tagged v0.16.18, which points at the same commit. The change is purely
a relabel -- no transitive impact -- but it lets downstream consumers
(litd, tapd, etc.) drop their replace directives, since v0.16.18 now
sorts above the previously-tagged v0.16.17 under Go's MVS.
2026-05-29 00:51:36 +08:00
yyforyongyu
12686ac3de
mod: bump kvdb to v1.5.1
Move from kvdb/v1.5.0 to the freshly tagged kvdb/v1.5.1, which points
at current master HEAD. The new tag includes the kvdb submodule's grpc
floor bump to v1.79.3 (173fd5147) and the otel SDK bump past the
GO-2026-4394 vulnerability (9978f4d33).

The root lnd module already requires the newer grpc and otel versions,
so this bump is effectively a relabel with no transitive impact on the
root build. But it keeps downstream consumers that import lnd/kvdb
directly from picking up v1.5.0 and pulling in the vulnerable otel SDK
through kvdb's submodule go.mod.
2026-05-29 00:51:25 +08:00
Yong
f21262dd6f
Merge pull request #10841 from ziggie1984/bump-grpc-v1.79.3
build: bump grpc to v1.79.3
2026-05-28 22:22:33 +08:00
Yong
d15a40c634
Merge pull request #10851 from ziggie1984/fix-fetch-nonterminal-pagination
sqldb: avoid materializing non-terminal payments
2026-05-28 22:16:35 +08:00
Yong
ca047dea66
Merge pull request #10857 from Roasbeef/drop-queue-kvdb-replaces
mod: drop queue + kvdb replaces, require tagged versions
2026-05-28 21:52:55 +08:00
Olaoluwa Osuntokun
d63bc2e237 mod: drop queue + kvdb replaces, require tagged versions
In this commit, we drop the in-tree replace directives for the queue and
kvdb submodules now that both have proper tags. queue/v1.2.0 carries the
BackpressureQueue[T] work, and kvdb/v1.5.0 completes the pgx/v4 -> pgx/v5
migration. Both tags point at the same commit where each module last
changed, so swapping the local copies for the tagged versions doesn't
change what we build.

We keep the sqldb replace for now: that one's still waiting on the gossip
V2 sqldb changes before we can cut its tag.
2026-05-28 11:59:27 +02:00
ziggie
9978f4d332
build: bump otel sdk past vulnerable version
Bump the root and standalone kvdb module OTel dependencies to v1.40.0,
which is the first release containing the fix for GO-2026-4394. This
also raises golang.org/x/sys to v0.40.0 through the updated module graph.

Keep the root and nested kvdb module metadata aligned so both normal and
kvdb_etcd builds resolve the fixed SDK version.
2026-05-27 16:50:48 -03:00
ziggie
173fd51473
kvdb: bump grpc module floor
Update the standalone kvdb module to resolve google.golang.org/grpc at
v1.79.3. The root module already requires this version, but kvdb can
also be tested or consumed as a nested module on its own.

Run go mod tidy in kvdb so the transitive requirements and checksums
match the grpc version selected by the module graph.
2026-05-27 16:40:03 -03:00
Yong
de8fc6ac9c
Merge pull request #10848 from saubyk/docs/db-migration-guide-update
docs: update db migration guide subsystem readiness for v0.21
2026-05-28 03:20:43 +08:00
Yong
7d97e2de83
Merge pull request #10845 from ziggie1984/add-tag-release-script
scripts: add tag-release.sh to safely cut release tags
2026-05-28 02:32:01 +08:00
ziggie
02daf8e96a
sqldb: avoid materializing non-terminal payments 2026-05-27 15:30:11 -03:00
ziggie
4fcde943aa
docs: mention release tag helper
Update the release branch management guide to use the release tag helper
instead of raw git tag commands.

This keeps the process aligned with the new script while leaving the final
push as an explicit maintainer step.
2026-05-27 08:52:18 -03:00
Olaoluwa Osuntokun
8cecb6204f
Merge pull request #10838 from ziggie1984/build-go-1.26.3
build: bump Go toolchain to 1.26.3
2026-05-27 13:27:54 +02:00
saubyk
9a260ee0a3
docs: update db migration guide subsystem readiness for v0.21
Mark the payments relational backend as available in v0.21 and refresh
the related guidance that previously told payment-heavy operators to wait
for the v0.21 release. Add btcwallet and channel state as in-progress
subsystems targeted for v0.22 (replacing the forwarding history entry),
and update the migration flow diagram and future improvements list to
match.
2026-05-26 18:34:17 -07:00
ziggie
2d1e05acd3
ci: add govulncheck binary scan 2026-05-26 15:45:16 -03:00
ziggie
d3dad1690d
build: bump Go version to 1.26.3 2026-05-26 15:45:16 -03:00
ziggie
e1f03b424f
release: validate version in release CI
The release workflow runs from pushed version tags, but the build step
was setting SKIP_VERSION_CHECK=1. That made scripts/release.sh exit
before running check-tag, so CI did not compare the pushed tag with
the version reported from build/version.go.

Run the normal release target instead. This keeps release CI from
producing artifacts when the tag and embedded lnd version drift apart.
2026-05-26 14:10:33 -03:00
Yong
58209e1f54
Merge pull request #10846 from guggero/block-dn-docs
docs: update docs after block-dn fixes
2026-05-27 00:29:44 +08:00
Oli
ff07a44d9a docs: update docs after block-dn fixes
The block-dn.org service now offers a */import/latest shortcut
for the block and filter header import, so a block height
doesn't need to be specified.

The issue on testnet3 was also fixed, so the warning in the
text is no longer required.
2026-05-26 17:30:50 +02:00
ziggie
5320aa0e36
scripts: add tag-release.sh to safely cut release tags
Adds a script that creates a signed annotated release tag only after
verifying:

  1. The requested tag name matches the version constants committed in
     HEAD:build/version.go. Catches the failure mode where a release
     branch is tagged before the version bump has been committed, which
     would otherwise leave the tagged commit reporting an old version
     string at runtime.

  2. The local HEAD is identical to the upstream lightningnetwork/lnd
     view of the release branch. A release tag must never point at a
     commit that has not been merged upstream yet.

The upstream remote is discovered by URL rather than by name, since
"origin" is conventionally the fork in a "gh repo fork" workflow. The
branch defaults to whichever one is currently checked out (typically a
release branch such as v0.21.x-branch) and can be overridden with
--branch.

The script deliberately does not push the tag or auto-bump version.go;
both remain explicit human steps.
2026-05-26 10:52:53 -03:00
Yong
34fbcf0576
Merge pull request #10813 from erickcestari/tor-v2-cleanup
Drop tor v2 onion production, keep wire codec faithful
2026-05-26 18:25:07 +08:00
ziggie
b32b67630f
build: bump grpc to v1.79.3 2026-05-25 14:04:09 -03:00
Yong
9f03672bda
Merge pull request #10828 from yyforyongyu/fix-bwcompat-route-wait
CI: fix sweep flake
2026-05-25 21:24:09 +08:00
Erick Cestari
fad749d02c
docs: document tor v2 onion handling in 0.21 release notes
Add a 0.21 release-notes entry covering the `--tor.v2` removal, the
boundary rejection of v2 input on operator entry points, the
persisted-state filtering (self-node announcement, watchtower client,
autopilot, graph bootstrapper, SCB restore), the Tor controller's
v3-only ADD_ONION restriction, and the wire-faithful behavior that
preserves peer-signed v2 entries through `lnwire`, `graph/db`, and
the graph RPCs.
2026-05-22 09:44:45 -03:00
Erick Cestari
2ae1db83b3
multi: drop tor v2 onion production, keep wire codec faithful
Tor stopped serving v2 onion services in October 2021; lnd should not
produce v2 addresses anymore, but it must still verify signatures on
and re-broadcast peer NodeAnnouncement messages that carry v2 entries.

Stop accepting v2 as configuration input (lncfg), strip the legacy
`--tor.v2` flag from the sample config, and remove the
`tor.OnionHostToFakeIP` helper. Operator entry points (`--externalip`,
`--listen`, `lncli connect`, `lncli wtclient towers add`) fail fast on
a v2 `.onion` string, so upgrading nodes must remove any v2 entry from
`lnd.conf` before lnd will start.

Filter persisted v2 state before use without rewriting on-disk records:
the self-announcement builder strips any v2 entry inherited from the
stored self-node; the watchtower client drops v2 entries from each
persisted tower's address list (skipping the tower entirely if no
non-v2 address remains); the autopilot connector, graph bootstrapper,
and static-channel backup restore paths skip v2 entries before
attempting outbound dials. Restrict the Tor controller's ADD_ONION
path to v3 keys, including the encrypted on-disk legacy-key fallback.

For inbound announcements, keep the wire codec wire-faithful:
`lnwire.WriteOnionAddr`, `graph/db.encodeOnionAddr`, and the matching
decoders round-trip v2 bytes so `DataToSign` reproduces the bytes the
remote peer signed, signature validation succeeds, and the announcement
is persisted to the graph DB and re-broadcast across restarts byte-for-
byte. RPC surfaces continue to expose the full address set so external
tools can independently reproduce and verify the signed bytes.

Add a netann regression test that signs a [v3, v2, ipv4] announcement,
round-trips it through Encode/Decode, verifies the signature, and
confirms the resulting models.Node preserves the v2 entry. Add a
graph bootstrapper test asserting v2 entries are skipped while v3 and
plain TCP entries on the same node still surface as bootstrap
candidates.
2026-05-22 09:42:37 -03:00
yyforyongyu
c2a25e1504
itest: assert sweep outpoint mined after RBF 2026-05-22 20:27:54 +08:00
Olaoluwa Osuntokun
cc49b37730
Merge pull request #10794 from Roasbeef/coop-close-insta-dispatch
contractcourt: insta-dispatch CLOSED_CHANNEL on first conf of coop close
2026-05-21 18:30:03 -07:00
Olaoluwa Osuntokun
f3d40f6239
Merge pull request #10552 from Roasbeef/neutrino-fast-sync
neutrino: add fast initial sync via header import
2026-05-21 16:55:42 -07:00
Olaoluwa Osuntokun
eaa2d6824e contractcourt+itest: tests for coop close insta-dispatch
In this commit, we add three focused unit tests in contractcourt
plus an itest that exercises the regression end-to-end.

The chain watcher harness gains an opt-in early-dispatch capture that
records every notifyEarlyCoopClose invocation so tests can assert how
many fired and what summaries they carried. On top of that:

TestEarlyDispatchCoopClose verifies the headline behavior. An
async-path coop close fires exactly one early dispatch with
IsPending=true and the post-N-conf flow still produces the regular
CooperativeCloseInfo downstream.

TestEarlyDispatchForceCloseNotInvoked guards the carve-out: force
closes never fire the early dispatch since their CLOSED_CHANNEL
event timing is intentionally unchanged.

TestEarlyDispatchReorgRefiresOnReReplacement nails down the reorg
path. Once a deep reorg removes the close, the early-dispatch flag
is cleared and the next coop close re-fires the early event with its
own summary, so a subscriber observes each distinct close attempt.

testZeroConfCoopCloseSubscribeEvents brings up a zero-conf channel
between Alice and Bob with --dev.force-channel-close-confs=3 so the
chain watcher takes the async multi-confirmation path. Alice
subscribes to channel events, initiates a cooperative close, and the
test asserts that CLOSED_CHANNEL fires after only one confirmation
of the close tx (not after the full three) and that
FULLY_RESOLVED_CHANNEL arrives once the close has reached three
confirmations. A quiet-window assertion at the end verifies that
exactly one CLOSED_CHANNEL event is delivered. If the suppression in
MarkChannelClosed broke and let it re-fire NotifyClosedChannel at N
confs, this assertion would catch the duplicate.
2026-05-21 14:04:26 -07:00
Olaoluwa Osuntokun
cae2de1f4d contractcourt+server: insta-dispatch CLOSED_CHANNEL on first conf
PR #10331 introduced a multi-confirmation reorg-aware dispatch in the
chain watcher. In production builds CloseConfsForCapacity is at least
3, so the chain watcher waits for three confirmations of a close tx
before running dispatchCooperativeClose, MarkChannelClosed, and
NotifyClosedChannel. Subscribers of the SubscribeChannelEvents stream
that used to receive a CLOSED_CHANNEL event after a single
confirmation in v0.20.1 stopped seeing the event entirely on shorter
test cycles and were delayed by two extra blocks on longer ones. This
is the regression alexbosworth reported on zero-conf channels.

The intent behind the original change was to wait three confirmations
under the hood for reorg safety while still dispatching a
CLOSED_CHANNEL event to RPC subscribers immediately, matching the
v0.20.1 surface. That insta-dispatch was wired into
peer.WaitForChanToClose for the local CloseChannel response stream
but was never extended to the channel-notifier path that drives
SubscribeChannelEvents.

In this commit, we wire a new optional notifyEarlyCoopClose callback
into the chain watcher's processDetectedSpend. The first time a coop
close spend is detected on chain, the chain watcher synthesizes a
ChannelCloseSummary with IsPending=true and dispatches a
CLOSED_CHANNEL event over the channel notifier, no DB round-trip
required. The callback is plumbed through ChainArbitratorConfig
.NotifyEarlyClosedChannel to the new
ChannelNotifier.NotifyEarlyClosedChannelEvent. The summary builder
shared with dispatchCooperativeClose is extracted into
buildCoopCloseSummary so the early and post-N-conf paths produce
equivalent payloads.

A coopCloseEarlyDispatched flag on the chain watcher keeps the
dispatch idempotent across blockbeat replays of the same spend, and
the closeObserver clears it on negativeConfChan so a re-mined or
replacement close after a deep reorg re-fires the preliminary event
with its own summary. The early-dispatch call sits before the
fast-path check so numConfs==1 also fires the early event through the
same code path.

Suppressing the duplicate notify at MarkChannelClosed time happens
inline in the chain_arbitrator MarkChannelClosed callback: after
CloseChannel succeeds, NotifyClosedChannel is fired only when the
close type is not CooperativeClose. Force, breach, and abandon paths
intentionally remain on the existing N-confirmation dispatch contract.
2026-05-21 13:49:10 -07:00
Olaoluwa Osuntokun
fc1391adb5 channelnotifier: add NotifyEarlyClosedChannelEvent
Today NotifyClosedChannelEvent rebuilds its event by round-tripping
through FetchClosedChannel, which forces the caller to have already
persisted the close summary to the closed-channel bucket. The chain
watcher needs to surface a CLOSED_CHANNEL event to RPC subscribers as
soon as a coop close spend is first detected on chain, well before
the close has reached the required confirmation depth at which the
state machine would normally call MarkChannelClosed.

In this commit, we add NotifyEarlyClosedChannelEvent, which dispatches
a ClosedChannelEvent built from a caller-supplied summary directly
through the subscribe server. The summary is expected to carry
IsPending=true so subscribers can recognize that the close has not
yet been finalized in the database.

Two unit tests assert that the new path delivers the supplied summary
verbatim and produces exactly one event per call.
2026-05-21 13:49:10 -07:00
Yong
0836954c4c
Merge pull request #10820 from yyforyongyu/fix-channel-open
funding: require explicit taproot channel negotiation
2026-05-21 17:36:07 +08:00
Olaoluwa Osuntokun
0aa1d8bdd6 multi: rename "taproot" channel type to mean production variant
In this commit, we shuffle the CLI and RPC names so the bare "taproot"
identifier refers to the production taproot channel type (final
scripts, feature bits 80/81), i.e. the variant new integrations should
actually be using. Before this commit, "taproot" on the CLI mapped to
the staging bits, and anyone who wanted a real production taproot
channel had to spell out "taproot-final" on `lncli openchannel` or
`SIMPLE_TAPROOT_FINAL` over RPC. The recommended choice was hidden
behind the longer name.

On the CLI (`lncli openchannel --channel_type=...`):

  - "taproot" now selects the production variant (it used to mean
    staging).
  - "taproot-staging" is added for the legacy development bits, for
    peers that haven't moved over yet.
  - "taproot-final" stays as a deprecated alias for "taproot" so
    existing scripts don't break.

On the RPC (`CommitmentType`):

  - `TAPROOT = 7` is added as the canonical name for the production
    type. `SIMPLE_TAPROOT_FINAL = 7` is kept as a deprecated alias via
    `option allow_alias = true`, so existing clients keep compiling
    against the same Go constant and the wire value doesn't change.
  - `SIMPLE_TAPROOT = 5` (staging) is unchanged.
  - `SIMPLE_TAPROOT_OVERLAY = 6` is unchanged. The taproot-assets
    daemon hard-codes this distinct enum value, so it's unaffected.

Wire compat is preserved end-to-end: only the comments, enum entry
order, and the CLI string-to-enum mapping change. The numeric values
and the existing generated Go identifiers stay stable.
2026-05-20 22:22:38 -07:00
yyforyongyu
654b680a2c docs: clarify taproot channel open defaults 2026-05-20 22:22:38 -07:00
yyforyongyu
70953e0818 funding: reject public taproot opens before sending 2026-05-20 22:22:38 -07:00
yyforyongyu
eb0c89beb1 funding: require explicit taproot channel negotiation 2026-05-20 22:22:38 -07:00
Olaoluwa Osuntokun
594024a312 docs: add neutrino headers import documentation
Document the neutrino fast sync feature that allows importing block and
filter headers from local files or HTTP URLs on startup. Cover
configuration for mainnet (block-dn.org), testnet3, testnet4, signet,
file format details, security considerations, and troubleshooting.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
9c0a219b33 itest: add neutrino headers import integration test
Add an integration test that verifies neutrino header import from local
files. The test mines blocks, starts a reference node to generate header
files via normal P2P sync, copies those files with import metadata, then
starts a new node configured to import headers from the prepared files.

The test verifies the import node syncs to the chain tip and can
continue syncing additional blocks mined after import via P2P, exercising
the hybrid import-then-P2P sync path.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
55f296cbd9 sample-lnd.conf: document neutrino header import options
Add commented examples for the new neutrino.blockheaderssource and
neutrino.filterheaderssource options, showing both URL-based import
from block-dn.org and local file path usage.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
f023a6a52a config_builder: wire neutrino header import config
Pass the configured BlockHeadersSource and FilterHeadersSource into
neutrino's HeadersImportConfig when initializing the neutrino backend.
Set blockchain.BFFastAdd validation flags for regtest and simnet to
skip contextual timestamp checks on rapidly-mined blocks.

Call Validate on the neutrino config before proceeding to catch
misconfiguration early.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
3f5edce093 lncfg: add neutrino header import source config fields
Add BlockHeadersSource and FilterHeadersSource fields to the Neutrino
config struct. These accept either local file paths or HTTP(S) URLs
pointing to pre-built header files for fast initial sync.

Add a Validate method that ensures both sources are specified together
or both are empty.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
88fff36e7c multi: update neutrino dep to include chainimport and header state fix
Update the neutrino dependency to a version that includes the
chainimport package for fast initial header sync, and the
ResetHeaderState fix that allows P2P sync to continue after import.

The updated neutrino dependency changes the ChainService.Start method
signature to accept a context.Context parameter. Update all call sites
to pass context.TODO() to maintain existing behavior.
2026-05-20 19:34:32 -07:00
Olaoluwa Osuntokun
2a4aab892a
Merge pull request #10803 from saubyk/docs/v0.21.0-testing-guide
docs: add v0.21.0 RC testing guides for humans and agents
2026-05-20 15:26:13 -07:00
Yong
2578566a1b
Merge pull request #10782 from jtobin/tapd-issue2108
chancloser: don't set ChanStatusCoopBroadcasted before close tx exists
2026-05-20 22:11:15 +08:00
ziggieXXX
93a48a4bc7
Merge pull request #10821 from ziggie1984/fix-backup-subscription-channel-updates
rpcserver: filter backup subscription events
2026-05-20 09:08:58 -03:00
Yong
46f8132f5b
Merge pull request #10823 from hieblmi/fix-probe-request
routerrpc: pass outgoing channels to probe requests
2026-05-20 16:55:18 +08:00
Slyghtning
546bcff7c9
routerrpc: pass outgoing channels to probe requests 2026-05-20 10:13:31 +02:00
Yong
3dde472b4c
Merge pull request #10771 from yyforyongyu/fix-lsp-probe-hash
routerrpc: isolate LSP route fee probes
2026-05-20 13:24:05 +08:00
ziggie
57b912c636
rpcserver: filter backup subscription events
Channel update notifications now flow through ChannelNotifier, so the
existing backup subscription deny-list started treating commitment
updates as backup-relevant events. This makes SubscribeChannelBackups
emit on every channel update, even though those updates can happen much
more frequently than lifecycle changes.

Switch the backup subscription to an allow-list of lifecycle events that
should trigger the stream. Also document that ChannelNotifier includes
high-frequency state updates, so lifecycle-only consumers should filter
explicitly.
2026-05-19 22:50:26 -03:00
Olaoluwa Osuntokun
5953c434fb
Merge pull request #10815 from Roasbeef/bip322-fix
lnwallet/rpcwallet: keep zero-value prev outputs in remote-sign PSBT
2026-05-19 18:13:54 -07:00
Olaoluwa Osuntokun
44f2ef7ef0
docs: release-note zero-value WitnessUtxo remote-sign fix
Add the bug-fix entry under 0.21.0 with a link to the PR.
2026-05-19 16:31:12 -07:00
Olaoluwa Osuntokun
6bb2c6f56c
lnwallet/rpcwallet: add unit tests for WitnessUtxo population helper
Cover the four resolution branches plus the BIP-322 regression case:

  - wallet-owns-it: FetchOutpointInfo returns a Utxo, helper writes
    the matching WitnessUtxo into the PSBT input.
  - external-fallback: wallet returns ErrNotMine, helper writes the
    WitnessUtxo from the sign descriptor's PrevOutputFetcher.
  - zero-value-fallback: same as above with the fetched entry's Value
    set to zero. This is the BIP-322 to_spend shape (input 0 of every
    BIP-322 to_sign references a virtual prev whose Value is mandated
    to be zero); the helper must populate the WitnessUtxo rather than
    silently skip it.
  - no-fallback: wallet returns ErrNotMine and no PrevOutputFetcher
    is provided; the helper leaves the input bare and the warning log
    fires (asserted only by absence of a populated WitnessUtxo).
  - empty-pk_script-fallback: the fetcher returns a non-nil entry
    with an empty PkScript; the helper rejects it as unusable (the
    PSBT WitnessUtxo serializer requires a non-empty script) and
    leaves the input bare.

The signed input (signDesc.InputIndex) is intentionally left untouched
by the helper — that input is the one the caller's main path will
populate later — and the tests cross-check that invariant on the
wallet-owns-it case.
2026-05-19 16:31:12 -07:00
Olaoluwa Osuntokun
9f31668b58
lnwallet/rpcwallet: accept zero-value WitnessUtxo entries in remote-sign prep
Before forwarding a SignOutputRaw request to the remote signer instance,
remoteSign rebuilds a PSBT from the unsigned transaction and annotates
every input with a WitnessUtxo (so the downstream walletkit.SignPsbt
call accepts it — taproot sighash computation requires the prev output
of every input, not just the one being signed).

For non-signed inputs the prep stage first asks the watch-only wallet
about the outpoint via FetchOutpointInfo, then — when the wallet does
not own or track the outpoint — falls back to the sign descriptor's
PrevOutputFetcher. The fallback previously required `utxo.Value != 0`,
which silently dropped legitimate zero-value entries on the floor and
left the corresponding PSBT input bare.

The walletkit.SignPsbt entry point on the remote signer then rejected
the PSBT with "input (index=N) doesn't specify any UTXO info" because
input N had neither a WitnessUtxo nor a NonWitnessUtxo annotation.

BIP-322 (signing virtual transactions for message attestation) is the
canonical hitter: its to_spend output is mandated by the BIP to be
exactly value=0 with the message commitment as pk_script, and that
output is referenced as input 0 of every BIP-322 to_sign transaction.
Any caller that drives a BIP-322 sign through a remote-signer LND
deployment was failing for this reason.

The validation we actually want is that the fetched prev output is
representable as a usable WitnessUtxo: non-nil and with a non-empty
pk_script. Drop the Value check; the zero-value case is well-formed
and the resulting PSBT input will serialize cleanly. The fetched-but-
empty-pk_script case continues to be rejected (a WitnessUtxo with
empty PkScript is malformed at PSBT serialization), and the warning
log when no fallback resolves the outpoint is preserved verbatim.

Lift the WitnessUtxo-population loop out of remoteSign into a
package-level helper so the resolution policy is unit-testable without
spinning up a real wallet + remote signer pair. The helper takes a
fetchOutpointInfoFn callback that mirrors
lnwallet.WalletController.FetchOutpointInfo. No behavior change for
the wallet-owns-it path or the no-fallback path.
2026-05-19 16:31:12 -07:00
ziggieXXX
361cdba0df
Merge pull request #10814 from erickcestari/deprecated-rpc-removal
Remove deprecated Send* / TrackPayment RPCs and outgoing_chan_id field
2026-05-19 13:00:55 -03:00
Erick Cestari
05bdb6633c
docs: document deprecated RPC and field removals in 0.21 release notes
Add entries to the Breaking Changes section covering the payment and
tracking RPCs and the `outgoing_chan_id` field removed in this branch,
all of which were announced for removal in 0.21 via the 0.20 release
notes.
2026-05-19 09:43:26 -03:00
Erick Cestari
d0768f0f78
proto: remove deprecated SendPayment, SendToRoute, TrackPayment RPCs
Remove the following deprecated RPC definitions that were announced for
removal in 0.21 via the 0.20 release notes:

lnrpc:
  - SendPayment (bidirectional streaming)
  - SendPaymentSync
  - SendToRoute (bidirectional streaming)
  - SendToRouteSync

routerrpc:
  - SendPayment (streaming)
  - SendToRoute
  - TrackPayment (streaming)

Also remove the now-unused PaymentState enum and PaymentStatus message
that were only used by the deprecated TrackPayment response stream, plus
the corresponding REST annotations from the yaml files.

Drop the now-orphan routerrpc.SendToRouteResponse message that was only
referenced by the deleted routerrpc.SendToRoute RPC.

Also remove the deprecated outgoing_chan_id field from
lnrpc.QueryRoutesRequest (tag 14) and routerrpc.SendPaymentRequest
(tag 8); their tag numbers are now reserved. Callers must use the
multi-channel outgoing_chan_ids field introduced in 0.20.

Drop the compat fallback in router_backend.go that previously consumed
the field, and regenerate all protobuf, gRPC, REST gateway, JSON, and
swagger files.
2026-05-19 09:43:26 -03:00
Yong
20c1dfce53
Merge pull request #10634 from Abdulkbk/om-fwding-tests
lnwire_test: include onion message in lnwire Benchmark
2026-05-19 20:36:47 +08:00
Abdullahi Yunus
9d5fab9a32
lnwire_test: add createValidTLVExtraData
In this commit, we added createValidTLVExtraData which creates a
valid TLV data, and use it in place of createExtraData for messages
that their Encode or Decode requires validating the TLV data, which
were failing initially.
2026-05-19 12:20:26 +01:00
Abdullahi Yunus
b2126c8440
lnwire_test: add onion message to makeAllMessages
In this commit, we include onion message to the list of lnwire
messages for benchmark testing.
2026-05-19 12:20:25 +01:00
Yong
ae616b5188
Merge pull request #10501 from asheswook/master
routerrpc: add outgoing_chan_ids to EstimateRouteFee
2026-05-19 16:40:44 +08:00
Roy Lee
11349b893f
docs: add release note for EstimateRouteFee outgoing chans 2026-05-19 16:41:41 +09:00
Erick Cestari
c7dd0a9965
routerrpc: remove deprecated SendPayment, SendToRoute, TrackPayment impls
Remove the SendPayment, SendToRoute, and TrackPayment shim methods from
router_server_deprecated.go that delegated to their V2 counterparts.
Remove their macaroon permission entries from router_server.go and the
now-unused legacyTrackPaymentServer wrapper.
2026-05-18 13:47:55 -03:00
Erick Cestari
3ff8791350
lnrpc: remove deprecated Send* RPC server implementations
Remove handler implementations and macaroon permission entries for the
now-deleted lnrpc RPCs: SendPayment, SendPaymentSync, SendToRoute, and
SendToRouteSync.

Also remove the dead payment infrastructure that was exclusively used by
these handlers: paymentStream, rpcPaymentRequest, rpcPaymentIntent,
extractPaymentIntent, dispatchPaymentIntent, sendPayment, and
sendPaymentSync.
2026-05-18 13:47:55 -03:00
Erick Cestari
a6c64ab9d0
itest: migrate deprecated lnrpc Send* calls to routerrpc V2
Remove SendToRoute and SendToRouteSync helpers from the test harness and
update integration tests to use routerrpc.SendToRouteV2:

- lnd_routing_test.go: collapse three SendToRoute test cases (sync,
  stream, v2) into a single test using SendToRouteV2; update
  testSendToRouteErrorPropagation to assert on Failure.Code instead of
  PaymentError string
- lnd_channel_policy_test.go: replace streaming SendToRoute with
  SendToRouteV2 and assert on HTLCAttempt.Failure instead of
  PaymentError string
2026-05-18 13:47:54 -03:00
Erick Cestari
d66341f3a9
routerrpc: remove deprecated outgoing_chan_id field handling
Remove the compatibility fallback in QueryRoutes and ExtractPaymentIntent
that accepted the deprecated single outgoing_chan_id field alongside the
replacement outgoing_chan_ids. Callers must now use outgoing_chan_ids.

Update TestQueryRoutes and TestExtractPaymentIntent accordingly.
2026-05-18 13:47:54 -03:00
Jaewook Lee
b24fb1720a
itest: add outgoing_chan_ids tests for EstimateRouteFee 2026-05-16 01:10:45 +09:00
Jaewook Lee
ea79df7a74
cmd: add outgoing_chan_id flag to estimatefee 2026-05-15 17:04:39 +09:00
Jaewook Lee
d625948e58
routerrpc: add outgoing_chan_ids to EstimateRouteFee 2026-05-15 17:04:39 +09:00
Olaoluwa Osuntokun
e8b75426db
Merge pull request #10799 from ziggie1984/fix-kv-payment-migration
payments/db: remap legacy zero attempt IDs
2026-05-14 10:31:11 -07:00
Jared Tobin
c5f3ad33f4
chancloser: remove dead ChannelFlushed.FreshFlush field
FreshFlush is never read in any transition handler. The only
producer (peer/brontide.go) sets it unconditionally to true,
and after the previous commit removed expectChanPendingClose,
the test loops that iterated over {true, false} no longer
differentiate between the two values.

Remove the field, the unconditional assignment, and collapse
the test loops into single sub-tests.
2026-05-14 10:21:29 -02:30
Jared Tobin
242df46400
docs: add release note for coop close limbo fix 2026-05-14 10:21:29 -02:30
Jared Tobin
ef7e084d93
chancloser: add nil-tx guard to legacy close test
Add call tracking to the legacy mockChannel so that every
MarkCoopBroadcasted invocation is recorded. TestTaprootFastClose
now asserts that at least one call was made and that every call
carried a non-nil tx, guarding against the limbo state described
in https://github.com/lightninglabs/taproot-assets/issues/2108.
2026-05-14 10:20:34 -02:30
Jared Tobin
711a4a400d
chancloser: remove MarkCoopBroadcasted(nil) calls
Remove the two call sites that set ChanStatusCoopBroadcasted
before a cooperative close transaction exists:

 - BeginNegotiation in the legacy close path (chancloser.go)
 - ChannelFlushed handling in the RBF close path
   (rbf_coop_transitions.go)

Both calls passed nil as the close tx, creating a "limbo" state
where ChanStatusCoopBroadcasted is set but no close transaction
is stored. This is unnecessary because ShutdownInfo — persisted
earlier by MarkShutdownSent in initChanShutdown / the RBF
ShutdownPending transition — already serves as the durable
signal that the shutdown flow was entered.

ChanStatusCoopBroadcasted should only be set when a real close
transaction exists, which this change preserves.
2026-05-14 10:20:29 -02:30
Yong
b9b299beca
Merge pull request #10729 from starius/pgx-v4-to-v5
multi: bump pgx/v4 -> pgx/v5, jackc/pgconn -> jackc/pgx/v5/pgconn
2026-05-14 15:35:10 +08:00
Yong
22cfb01251
Merge pull request #10686 from starius/fix-slow-peerinfo-healthcheck4a
chainreg: use getnetworkinfo to count outbound peers (bitcoind backend)
2026-05-14 14:26:36 +08:00
Boris Nagaev
565cf11550
lnd: bump pgx/v4 to pgx/v5 in main module
Update the main module's pgxpool usage from pgx/v4 to pgx/v5:
- Change import from pgx/v4/pgxpool to pgx/v5/pgxpool
- Update pgxpool.Connect to pgxpool.New (v5 API change)
- Remove pgx/v4 and its transitive dependencies from go.mod

Note: pgxpool.New is lazy, but this is done in tests only and the very next
line is pool.Exec, so if there is a connection error, it will be caught there.
2026-05-13 23:14:31 -05:00
Boris Nagaev
59ab677d11
go.mod: add temporary replace directive for kvdb
Add a local replace directive for kvdb so the main module can reference
the local kvdb changes (pgx/v5 migration) before a new kvdb version is
released.
2026-05-13 23:14:31 -05:00
Boris Nagaev
848af80354
kvdb: bump pgx/v4/stdlib to pgx/v5/stdlib
Update the SQL driver registration import from pgx/v4/stdlib to
pgx/v5/stdlib, completing the migration of the kvdb module away from
the deprecated pgx v4.
2026-05-13 23:14:31 -05:00
Boris Nagaev
548233fbfd
sqldb/v2: use pgx/v5/pgconn instead of standalone pgconn
In pgx v5, the pgconn package was absorbed into the main pgx module.
Update imports from github.com/jackc/pgconn to
github.com/jackc/pgx/v5/pgconn and remove the now-unnecessary
standalone pgconn dependency from go.mod.
2026-05-13 23:14:30 -05:00
Boris Nagaev
e9b422f785
sqldb: use pgx/v5/pgconn instead of standalone pgconn
In pgx v5, the pgconn package was absorbed into the main pgx module.
Update imports from github.com/jackc/pgconn to
github.com/jackc/pgx/v5/pgconn and remove the now-unnecessary
standalone pgconn dependency from go.mod.
2026-05-13 23:14:30 -05:00
Boris Nagaev
e9098c1e35
docs: note bitcoind peer-check RPC switch
Document that bitcoind outbound peer health checks now use
getnetworkinfo.connections_out instead of getpeerinfo.

Also mention that texts of zmq port mismatch warnings were fixed.
2026-05-13 23:13:13 -05:00
Boris Nagaev
9358ca326b
chainreg: clarify zmq port-mismatch warnings
The previous warning text ("unable to subscribe to zmq ... events") suggested
that lnd failed to create the ZMQ connection, when in reality it only means the
configured port differs from what bitcoind reports via getzmqnotifications.
Reword both messages to say "port mismatch" and tell to verify the port.

Fixes https://github.com/lightningnetwork/lnd/issues/10568
2026-05-13 23:12:34 -05:00
Boris Nagaev
dae00811d1
chainreg: use getnetworkinfo for peer-count
Use getnetworkinfo.connections_out for bitcoind outbound peer checks instead
of getpeerinfo. This keeps the isolation-safety signal while avoiding heavier
per-peer work.

This helper is bitcoind-specific, btcd does not currently implement it.
2026-05-13 23:12:34 -05:00
Boris Nagaev
1c74c384dc
chainreg: use typed getzmqnotifications RPC
Simplify the code. Use rpcclient's GetZmqNotifications method instead of a raw
request and manual unmarshalling when validating bitcoind ZMQ subscriptions.
The typed result already parses notification addresses, so the extra per-entry
URL parsing is removed.
2026-05-13 23:12:34 -05:00
Boris Nagaev
25188236b4
chainreg: use GetNetworkInfo for version check
Simplify the code. Use rpcclient's typed GetNetworkInfo call in
getBitcoindHealthCheckCmd instead of a raw JSON-RPC request and
manual JSON decoding.
2026-05-13 23:12:34 -05:00
Yong
cc65a20335
Merge pull request #10790 from ziggie1984/chanstate-sql-migration-pr2-consumers
chanstate: migrate consumers to Store interface (PR 2 of channel-state decomposition)
2026-05-14 11:45:51 +08:00
Yong
066669aec9
Merge pull request #10800 from ziggie1984/bump-neutrino-0f87fa7
mod: bump neutrino dependency
2026-05-14 03:59:25 +08:00
saubyk
2d0187c6a5
docs: add v0.21.0 RC testing guides for humans and agents
Adds docs/testing-guides/v0.21.0/ with a per-feature guide for the
v0.21.0-beta.rc1 release. Each guide follows a fixed template
(prereqs, setup, scenarios with concrete pass/fail signals, failure
investigation) so RC testers and automated agents can work through
them predictably.

Coverage:
- Headline features: production taproot channels, RBF taproot
  coop-close, payment store KV->SQL migration, onion messaging +
  rate limiting.
- High-risk regressions / breaking changes: closed-channel
  tombstone (sqlite/postgres downgrade trap), reorg-safe channel
  closes + MinCLTVDelta raise, chain_params network-mismatch DB
  guard, GetDebugInfo log opt-in.
- New RPCs / operator features: payment-adjacent RPCs bundle,
  multiple read-only middleware interceptors.

This is a first draft intended for community review on the PR.
payment-sql-migration.md carries a TBD callout for the
SkipNativeSQLMigration rescue-path behavior, pending developer
confirmation.
2026-05-12 21:30:50 -07:00
Elle
1c5df12ffa
Merge pull request #10796 from ellemouton/for-each-node-cached-autopilot
autopilot+graph/db: drop unused address loading from cached node iteration
2026-05-12 16:00:53 -07:00
Elle Mouton
c18f139e24
docs: add release note for autopilot graph-cache fix
Note the performance improvement from dropping the unnecessary address
load on the SQL backend and letting the kvdb in-memory graph cache
serve autopilot's scoring traversal.
2026-05-12 12:39:51 -07:00
Elle Mouton
d0e9042944
autopilot: pass pubkey in channel traversal
ForEachNodesChannels is a topology traversal: its callers only need
the node identity plus channel edges. After removing address loading
from ForEachNodeCached, constructing a Node for this callback is
misleading because Addrs is either unused or empty.

Pass NodeID directly through the interface and update the scoring
and simple graph callers to use that pubkey. This keeps the
address-bearing Node interface on ForEachNode, where autopilot
gathers connectable candidates and their addresses before dialing.
2026-05-12 12:39:51 -07:00
ziggie
037549d994
sample-conf: sync neutrino useragentversion with upstream
The neutrino bump updates the default UserAgentVersion from
"0.12.0-beta" to "0.16.2", which made the sample-conf check fail.
2026-05-12 15:15:03 -03:00
ziggie
80d04a50b8
mod: bump neutrino dependency 2026-05-12 15:15:03 -03:00
Yong
3ba3eaaff4
Merge pull request #10612 from Abdulkbk/onion-msg-path
onionmessage: graph-based pathfinding for onion messages
2026-05-13 00:42:38 +08:00
ziggie
c11a292e35
payments/db: remap legacy zero attempt IDs
Legacy KV payments can contain HTLC attempts with attempt ID zero. The
SQL payments schema requires payment_htlc_attempts.attempt_index to be
globally unique, so migrating multiple such attempts can fail with a
UNIQUE constraint violation.

Allocate synthetic attempt indexes for legacy zero-ID attempts from the
switch payment ID sequencer horizon. Keep nonzero attempt IDs unchanged
and advance the switch sequence once after migration validation succeeds.

This preserves the SQL uniqueness invariant and prevents future switch
IDs from colliding with migrated attempts. It also wraps HTLC insert
errors with the attempted index and payment hash so future migration
failures identify the problematic row.
2026-05-12 13:19:41 -03:00
ziggie
0376270e5c
server: depend on chanstate Store
Store channel-state access on server as chanstate.Store instead of
*channeldb.ChannelStateDB. Keep link-node access as a separate concrete
*channeldb.LinkNodeDB field so LinkNodeDB does not leak into the
channel-state store contract.
2026-05-12 11:26:28 -03:00
ziggie
186ea36f37
lnrpc: depend on chanstate Store
Replace concrete ChannelStateDB fields in the invoices and wallet RPC
configs with chanstate.Store, and update the subserver dependency
wiring to pass the interface. The affected RPC paths only need
channel-state store methods for hop hints and waiting-close channel
queries.
2026-05-12 11:26:28 -03:00
ziggie
2e7a1d8aac
lnd: use chanstate Store for channel restore
Make chanDBRestorer persist restored channel shells through
chanstate.Store instead of the concrete ChannelStateDB. The restorer
still builds channeldb channel shell values, but only needs
RestoreChannelShells from the store.
2026-05-12 11:26:27 -03:00
Yong
3adf45d3ab
Merge pull request #10777 from ziggie1984/chanstate-sql-migration-pr1-interface
chanstate: introduce Store interface (PR 1 of channel-state decomposition)
2026-05-12 11:27:34 +08:00
Yong
530ebccfbb
Merge pull request #10765 from erickcestari/enforce-bolt02-push-msat-bound
funding: enforce BOLT-02 push_msat bound on fundee
2026-05-12 09:41:02 +08:00
ziggie
8f10cf5412
contractcourt: use chanstate Store in breach arb
Replace BreachConfig's concrete ChannelStateDB dependency with
chanstate.Store. The breach arbitrator only needs closed-channel reads
and MarkChanFullyClosed from the channel-state store.
2026-05-11 18:54:31 -03:00
ziggie
96e0fd7447
chanstate: introduce Store interface
Add a new chanstate package containing the Store interface plus a
package logger. The interface mirrors the public surface of
*channeldb.ChannelStateDB so the compile-time assertion

    var _ ChannelStore = (*channeldb.ChannelStateDB)(nil)

No consumer migrates in this commit.
2026-05-11 18:54:30 -03:00
ziggie
1f4a756f5e
channelnotifier: depend on chanstate Store
Accept chanstate.Store in ChannelNotifier instead of the concrete
ChannelStateDB. The notifier only fetches open and closed channel
records to populate channel event payloads.
2026-05-11 18:54:30 -03:00
ziggie
d08b1b07c7
peer: depend on chanstate Store
Replace the peer config's concrete channel state DB dependency with
chanstate.Store. Brontide only needs channel lookups, closed-channel
lookup, and initial forwarding policy access from the channel-state
store.
2026-05-11 18:54:30 -03:00
ziggie
ae9de8d078
funding: depend on chanstate Store
Replace the funding manager's concrete *channeldb.ChannelStateDB
dependency with chanstate.Store. The manager only uses methods covered
by the store contract, including channel opening state and initial
forwarding policy persistence.
2026-05-11 18:54:30 -03:00
ziggie
bb8eeb2304
channeldb: drop unused kvdb.RTx parameter from FetchChannelByID
All four call sites pass nil for tx today (server.go, two in
channeldb/db_test.go, funding/manager_test.go). The internal
channelScanner(nil, selector) call inside FetchChannelByID is preserved
verbatim, so runtime behavior is unchanged.

This is a prerequisite for the upcoming chanstate.ChannelStore
interface: keeping the parameter would leak kvdb into a domain
interface.
2026-05-11 18:03:10 -03:00
Erick Cestari
bb8abc7516
docs: release note for BOLT-02 push_msat fundee enforcement
Document the user-visible error string change introduced when the
fundee rejects incoming open_channel messages whose push_msat exceeds
1000 * funding_satoshis.
2026-05-11 14:22:10 -03:00
Erick Cestari
d5505498c4
funding: enforce BOLT-02 push_msat bound on fundee
Reject incoming OpenChannel messages where push_msat exceeds
1000 * funding_satoshis, as required by BOLT-02. The existing RejectPush
flag only gates on push_msat > 0 and does not cover the spec bound.

An over-sized push is eventually caught downstream in reservation.go when
theirBalance = capacity - fee - push_msat goes negative and
ErrFunderBalanceDust is returned. Rejecting it up front produces a
clearer, spec-aligned error and avoids the chanacceptor and commitment
type negotiation round-trips for a channel we will refuse anyway.
2026-05-11 14:20:39 -03:00
Elle Mouton
bfb12b1d81 graph/db: remove address loading from cached node iteration
ForEachNodeCached is now only used for topology-oriented traversal,
so the address-loading option forced one autopilot scoring path to
bypass the in-memory graph cache for data it did not consume. Remove
the withAddrs parameter and the associated SQL/KV address plumbing
so cached node iteration can consistently use the graph cache when
it is loaded.

Autopilot still requires peer addresses before opening channels.
That filtering remains in Agent.openChans via ForEachNode, where the
selected candidates' addresses are collected for ConnectToPeer. The
trade-off is that ForEachNodesChannels no longer excludes
addressless nodes from graph-wide scoring inputs such as median
channel size or centrality, which also feed lncli getnetworkinfo
statistics like graph diameter. In practice the only addressless
nodes our local view tends to know about are nodes with no public
channels (e.g. our own node or peers we share only private channels
with), so the impact on the reported stats should be negligible.
Active channel candidates remain address-filtered before dialing.
2026-05-07 16:29:12 -07:00
Olaoluwa Osuntokun
50eb0d1466
Merge pull request #10793 from aakselrod/fix-sqlbase-nonconst-fmt-strings
kvdb/sqlbase: fix non-constant format strings
2026-05-07 13:31:52 -05:00
Alex Akselrod
ce65753d0f
kvdb/sqlbase: fix non-constant format strings
When updating the Go version in kvdb to >= 1.24, schema.go no
longer compiles due to non-constant format strings. This is
invisible until:
- a new version of kvdb is tagged and imported in consumers
- kvdb is redirected to the local copy

This commit fixes the bug.
2026-05-06 13:09:59 -07:00
ziggieXXX
6fd5b7bb27
Merge pull request #10780 from ziggie1984/channeldb-tombstone-close
channeldb: tombstone closed channels on KV-SQL backends
2026-05-04 12:11:00 -03:00
ziggie
1a52e85b94
docs: release note for closed-channel tombstones on KV-SQL backends
Document the tombstone close path that #10780 wires onto sqlite/postgres,
the operator-visible iteration-cost growth that comes from leaving closed
chanBuckets on disk, and the NumForwardingPackages divergence that the
preserved forwarding-package bucket produces in PendingChannels.
2026-05-04 09:46:33 -03:00
ziggie
9504412478
multi: enable closed-channel tombstones on KV-SQL backends
Wire OptionTombstoneClosedChannels for sqlite and postgres backends in
config_builder.go. bbolt keeps the synchronous one-shot close path
unchanged.

Add UsesClosedChanTombstones() to the integration test harness so
backend-symmetric tests can skip post-close assertions about deleted
forwarding-package or revocation-log state — that state is intentionally
preserved on tombstone backends until the upcoming native-SQL
channel-state migration reclaims it. Update testWipeForwardingPackages
to honor the new predicate while still exercising the close flow on
both backend families.
2026-05-04 09:46:32 -03:00
ziggie
dc4b851454
channeldb: hide tombstoned channels from open views
Wire every reader of openChannelBucket to consult isOutpointClosed
before treating a chanKey as open. Without this commit the previous one
flips the outpoint index but FetchAllChannels and friends still surface
the channel as if it were open — that intermediate state is fine for
tests because OptionTombstoneClosedChannels stays off until the multi:
commit flips it on for sqlite/postgres, but the readers must be wired
before that lands.

Audit covers all six call sites that descend into chanBucket:

  - fetchChanBucket / fetchChanBucketRw — direct lookup paths used by
    Refresh, MarkBorked, and the rest of OpenChannel's read/write
    methods. Single-call sites; the tx.ReadBucket(outpointBucket)
    lookup is inlined into the isOutpointClosed call.
  - fetchNodeChannels — per-node ForEach iteration; tx threaded
    through and the outpoint-bucket lookup is hoisted above the loop
    so the closed-channel check is a per-iteration map probe rather
    than a tx-level bucket resolve.
  - FetchPermAndTempPeers — cross-node ForEach; same hoisting pattern.
    The closed peer's protected status is still established by the
    historical-channel second pass that runs after the open-channel
    pass.
  - channelScanner — single-channel-selector iteration site reached by
    FetchChannel and FetchChannelByID; outpoint-bucket lookup hoisted
    inside chanScan so a single visit pays the bucket-resolve cost
    once.

The redundant-close guard added in the previous commit lives in
locateOpenChannel and is unchanged here.

Tests:
  - TestCloseChannelTombstoneRemovesFromOpenScans: end-to-end —
    FetchAllChannels, FetchOpenChannels, and FetchPermAndTempPeers all
    behave as if the closed channel is gone, while the historical pass
    still marks the peer as having a closed channel.
  - TestClosedChannelHiddenFromFetchChannel: channelScanner path.
  - TestClosedChannelHiddenFromDirectMethods: fetchChanBucket /
    fetchChanBucketRw via Refresh and MarkBorked.
2026-05-04 09:46:32 -03:00
ziggie
d643050dfb
channeldb: tombstone closed channels without bulk delete
Wire the tombstone close path on backends that opted in via
OptionTombstoneClosedChannels:

  - ChannelStateDB.CloseChannel branches on tombstoneClosedChannels.
    The default path remains closeChannelSync; closeChannelTombstone
    runs on tombstone-enabled backends.
  - closeChannelTombstone leaves every byte of the channel's nested
    state in place — chanBucket, revocation log, per-channel
    forwarding-package bucket, commitment heads — and relies on the
    outpointBucket flip from outpointOpen to outpointClosed (already
    performed by the shared updateClosedOutpointIndex helper) as the
    authoritative closed-channel marker. The historical-channel and
    close-summary archival use the same archiveClosedChannel helper as
    the synchronous path so closed-channel and historical readers see
    uniform records regardless of backend.
  - locateOpenChannel rejects already-closed chanKeys (outpointClosed
    in the index) with ErrChannelNotFound so a redundant CloseChannel
    is a no-op rather than a re-archive.

Open-channel-bucket readers still surface tombstoned channels — that
audit lands in the next commit. The tests added here only assert the
writer's on-disk artefacts and the redundant-close guard.

Tests:
  - TestCloseChannelTombstoneWritePath: outpoint flipped, historical
    record, close summary, revlog/fwd-pkgs preserved.
  - TestCloseChannelTombstoneRedundantClose: second CloseChannel
    returns ErrChannelNotFound.
  - TestCloseChannelSync: regression test for the synchronous path —
    chanBucket and fwd-pkgs gone, outpoint flipped.
2026-05-04 09:46:32 -03:00
ziggie
f94fba1e85
channeldb: add tombstone option and isOutpointClosed helper
Add the option, field, and reader helper that the tombstone close path
will consume in the next commit. Nothing is wired yet:

  - OptionTombstoneClosedChannels — option modifier that sets the new
    Options.tombstoneClosedChannels field. Defaults to off.
  - ChannelStateDB.tombstoneClosedChannels — the decision-bit, set at
    construction from the option.
  - isOutpointClosed(opBucket, chanKey) — decodes the indexStatus TLV
    stored under outpointBucket and reports true for entries flipped to
    outpointClosed by updateClosedOutpointIndex (called from both close
    paths). The helper accepts the bucket directly so loop-style
    callers can hoist the tx.ReadBucket(outpointBucket) lookup out of
    the inner loop and pay it once per iteration set.

Reusing outpointBucket as the "logically closed" signal avoids a
dedicated tombstone bucket. The flip from outpointOpen to outpointClosed
is already performed by the existing close path, so the signal exists on
both backends — readers just need to consult it.

Behavior is unchanged. The option and helper have no callers yet; the
close-path branch and reader audit land in the next two commits.
2026-05-04 08:58:32 -03:00
Yong
8df972d965
Merge pull request #9821 from lightningnetwork/coop-close-actor
multi: add new rbf coop close actor for RPC server fee bumps
2026-05-01 03:53:31 +08:00
ziggie
4815ed2b23
channeldb: extract close-channel helpers
Move the body of OpenChannel.CloseChannel into ChannelStateDB.CloseChannel
(which dispatches to a new closeChannelSync method), and split the close
logic into three free helpers:

  - locateOpenChannel: descends the open-channel bucket tree and returns the
    chain bucket, channel bucket, and serialized chanKey for an OpenChannel.
  - updateClosedOutpointIndex: flips the outpoint index entry for a chanKey
    from open to closed.
  - archiveClosedChannel: writes the historical-channel record and the close
    summary that survive the close.

Behavior is preserved: closeChannelSync runs the same sequence of mutations
(packager wipe, chanBucket delete, log-bucket delete, outpoint flip,
historical archive, close summary) that the inline body did, just composed
out of the new helpers. No callers, options, or readers change.

This is preparation for adding a tombstone close strategy on KV-SQL
backends; the helpers will be shared between the synchronous and tombstone
paths so historical and closed-channel records remain identical across
backends.
2026-04-30 09:08:00 -03:00
Abdullahi Yunus
72873f6275
docs: add release not for pathfinding 2026-04-29 22:34:44 +01:00
Abdullahi Yunus
fd25ba9853
onionmessage: add BFS pathfinding for onion messages
In this commit we add FindPath, a BFS-based shsortest-path
algorithm that finds routes through the channel graph for
onion messages. The search filters nodes by the
OnionMessage feature bits (38/39).

We also add a  unit tests covering: direct neighbor routing,
multi-hop paths, feature-bit filtering, missing destination
nodes, destination without onion support, max hop limits,
cycle handling, and shortest-path selection.
choice of BFS is because there isn't any weight involve.
2026-04-29 22:34:44 +01:00
ziggieXXX
746ed3c137
Merge pull request #10776 from ziggie1984/fix-release-notes-0.21.0-contributors-anchor
docs: fix Contributors TOC anchor in v0.21.0 release notes
2026-04-28 19:22:40 -03:00
ziggie
c0b417c79c
docs: fix Contributors TOC anchor in v0.21.0 release notes
The TOC link for the Contributors section pointed to `#contributors`,
but GitHub generates the anchor for `# Contributors (Alphabetical
Order)` as `#contributors-alphabetical-order`, leaving the link
broken when the rendered file is viewed on GitHub. Update the TOC
to use the working anchor (matching the form already used in
release-notes-0.18.0.md).
2026-04-28 16:40:23 -03:00
ziggieXXX
caccaf3cf1
Merge pull request #10775 from ziggie1984/release-notes-0.22.0
docs: add empty release notes scaffold for v0.22.0
2026-04-28 15:24:48 -03:00
ziggie
fa588eaf1a
docs: add empty release notes for v0.22.0
Scaffold release-notes-0.22.0.md with the same section structure as
the 0.21.0 file so contributors have a place to land entries during
the v0.22 cycle.
2026-04-28 11:45:07 -03:00
ziggieXXX
1ddcd081ed
Merge pull request #10768 from yyforyongyu/9985-breach-arb
contractcourt: make breach retribution final-taproot aware
2026-04-27 21:35:02 -03:00
Yong
5b85551a97
Merge pull request #10725 from ziggie1984/lnwallet/fix-neutrino-reorg-wallet-sync-timeout
lnwallet/test: fix flaky neutrino reorg sync timeout
2026-04-27 13:21:33 +08:00
yyforyongyu
8eab1edd69
docs: add EstimateRouteFee release note 2026-04-27 09:38:48 +08:00
yyforyongyu
1dd09d2300
routerrpc: isolate LSP route fee probes 2026-04-27 09:38:48 +08:00
Yong
af1fc43fb7
Merge pull request #10689 from ziggie1984/bump-bitcoind-v30
scripts+GitHub: use bitcoind v30.0
2026-04-27 09:33:06 +08:00
ziggie
3cf60bd474
lnwallet/test: fix flaky neutrino reorg sync timeout
waitForWalletSync used time.Tick inside the poll loop, leaking a new
goroutine on every iteration. Over 5 reorg cycles with ~300 polls each
this accumulated up to 1500 leaked goroutines, adding measurable system
load that made the 30s timeout too tight, especially when running against
a postgres backend where block-processing writes carry more overhead.

Fix the leak by using a single time.NewTicker (deferred Stop), and raise
the timeout to 2 minutes to give the neutrino P2P layer and the
address-manager transaction walk enough headroom under load.

Also improve the timeout error messages to identify which of the two
sync layers was stuck:
- Layer 1 (header/P2P): ChainIO.GetBestBlock height has not yet caught
  up to the miner tip — neutrino is still fetching headers.
- Layer 2 (transaction walk): heights matched but IsSynced() never
  returned true — the chain-sync notification or the address-manager
  DB write (undo+redo on reorg) did not complete in time.

Add a detailed doc comment to waitForWalletSync explaining the three
pipeline stages (header sync, compact-filter/block fetch, transaction
walk) and why each stage is relevant, so a future timeout can be
diagnosed from the error message alone.
2026-04-26 22:32:33 -03:00
Yong
acb8f1c92e
Merge pull request #10715 from yyforyongyu/fix/ci-sweep-watchtower-flakes
itest+lntest: harden sweep and watchtower flakes
2026-04-27 09:19:57 +08:00
yyforyongyu
4d374fb5df
contractcourt: make breach retribution final-taproot aware 2026-04-24 18:48:31 +08:00
ziggie
651e5e6e7b
GitHub: bump itest bitcoind to v31
Bitcoin Core v31.0 was released on 2026-04-20. Update the CI itest
matrix to track the latest major release.
2026-04-23 11:33:50 -03:00
ziggie
6914ea0b1a
lntest: pin pre-v30 mempool policy defaults in itest bitcoind
Bitcoind v30 lowered the default minrelaytxfee and incrementalrelayfee
from 1000 sat/kvB (1 sat/vB) to 100 sat/kvB. The itest suite was
written against the old defaults and the lower values cascade into:

  - integer sat/vByte assertions losing precision below 1 sat/vB, and
  - RBF bump thresholds that alter sweeper/bumpfee replacement timing.

Pin the old defaults in the itest bitcoind backend so the existing
tests keep passing without per-test adaptation. Running against the
new defaults is still worth doing, but that is a separate exercise
that should not be bundled with the v30 version bump.
2026-04-23 11:27:41 -03:00
ziggie
154e507675
testing: allow patch versions of bitcoin core 2026-04-23 11:27:41 -03:00
Olaoluwa Osuntokun
07f54ae173 lnwallet/chancloser: create unique ServiceKey for the RBF chan closer
This can be used to allow any system to send a message to the RBF chan
closer if it knows the proper service key. In the future, we can use
this to redo the msgmux.Router in terms of the new actor abstractions.
2026-04-22 17:33:04 -07:00
Olaoluwa Osuntokun
ac4bc2392d protofsm: implement the actor.ActorBehavior interface for StateMachine
In this commit, we implement the actor.ActorBehavior interface for
StateMachine. This enables the state machine executor to be registered
as an actor, and have messages be sent to it via a unique ServiceKey
that a concrete instance will set.
2026-04-22 17:33:04 -07:00
Olaoluwa Osuntokun
fa2d0f9904 peer: register the rbfCloseActor, have RPC route fee bumps to it
In this commit, we now register the rbfCloseActor when we create the rbf
chan closer state machine. Now the RPC server no longer neesd to
traverse a series of maps and pointers (rpcServer -> server -> peer ->
activeCloseMap -> rbf chan closer) to trigger a new fee bump.

Instead, it just creates the service key that it knows that the closer
can be reached at, and sends a message to it using the returned
actorRef/router. We also hide additional details re the various methods
in play, as we only care about the type of message we expect to send and
receive.
2026-04-22 17:33:04 -07:00
Olaoluwa Osuntokun
2a3ae1efe8 peer: create new rbfCloseActor to decouple RPC RBF close bumps
In this commit, we create a new rbfCloseActor wrapper struct. This will
wrap the RPC operations to trigger a new RBF close bump within a new
actor. In the next commit, we'll now register this actor, and clean up
the call graph from the rpc server to this actor.
2026-04-22 16:46:55 -07:00
ziggie
752ad163fa
scripts+GitHub: use bitcoind v30.0 2026-04-21 21:56:04 -03:00
Yong
a8a3e13120
Merge pull request #10763 from GeorgeTsagk/taproot-final-acceptor
multi: fix SIMPLE_TAPROOT_FINAL acceptor and overlay RBF auto-enable
2026-04-21 16:47:39 +08:00
George Tsagkarelis
33f623c855
docs: add release note for SIMPLE_TAPROOT_FINAL follow-ups
See https://github.com/lightningnetwork/lnd/pull/10763.
2026-04-20 16:11:31 +02:00
George Tsagkarelis
6d95154801
server: do not auto-enable RBF coop close for overlay channels
An earlier commit added an auto-enable that forces RbfCoopClose=true
whenever either taproot channel flag is set. This breaks
taproot-overlay channels, because the RBF coop close state machine in
lnwallet/chancloser/rbf_coop_*.go does not integrate the AuxCloser
(or any other aux) hook that overlay channels depend on to build
aux-aware close transactions. A node that enables
--protocol.simple-taproot-overlay-chans ends up with RBF force-on and
its overlay channel closes silently fail, leaving the aux closer
unable to finalize on-chain.

Narrow the auto-enable so it only fires for TaprootChans (staging /
final taproot) and explicitly skips it when TaprootOverlayChans is set.
Operators that positively want RBF can still opt in via
--protocol.rbf-coop-close; this change only removes the forced path
that silently breaks overlay closes.
2026-04-20 16:00:26 +02:00
George Tsagkarelis
58231f1ff8
chanacceptor: map SIMPLE_TAPROOT_FINAL in rpc acceptor
The feature-bits-to-lnrpc-enum switch in sendAcceptRequests covered
every commitment type the RPC acceptor can be asked about, except the
production taproot variant introduced alongside the prod-taproot-chans
work. For a channel open using SimpleTaprootChannelsRequiredFinal (with
any combination of the scid-alias / zero-conf modifiers), the switch
fell through to the default branch, which logs a warning and leaves
commitmentType at its zero value -- lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE.
External acceptor clients then see UNKNOWN rather than the actual
commitment type and either reject or misclassify the channel.

Add the four missing cases so the new commitment type is reported to
acceptor clients correctly.
2026-04-20 16:00:25 +02:00
Yong
8f73764547
Merge pull request #10762 from yyforyongyu/update-release-notes
docs: add missing 0.21.0 contributors
2026-04-20 16:07:27 +08:00
yyforyongyu
e12435ef0c
docs: update contributor list 2026-04-20 13:33:38 +08:00
ziggieXXX
ba32f2e7a8
Merge pull request #10741 from saubyk/update-code-contribution-guideline
docs: add new contributors section to contribution guidelines
2026-04-17 22:18:31 +02:00
saubyk
2e7faf0e16
docs: add new contributors section to contribution guidelines
Advise new contributors to build a PR review track record before
submitting code, or to open a detailed issue when they spot a bug.
2026-04-17 10:54:45 -07:00
Olaoluwa Osuntokun
3accc51119
Merge pull request #10754 from gijswijs/onion-msg-block-same-peer-cycle
onionmessage: drop onion messages cycling back to the sending peer
2026-04-17 10:10:02 -07:00
Gijs van Dam
84ec3cb233 docs: add release note for same-peer onion message cycle drop
Record the onion-message same-peer cycle drop from #10754 under a new
Robustness subsection in the 0.21.0 release notes.
2026-04-17 12:07:57 -05:00
Gijs van Dam
261babcf09
onionmessage: drop onion messages cycling back to the sending peer
Block forwarding of an onion message when the resolved next hop is the
same peer that delivered it. Such a forward would immediately bounce the
message back over the very connection it arrived on, which is never
useful and can be abused to amplify traffic against a peer.

The check runs after the routing action is resolved, so both direct
next-node-ID and SCID-resolved paths are covered. A new
`ErrSamePeerCycle` is returned (and logged at warn level) when a cycle
is detected.
2026-04-17 12:33:08 +02:00
Olaoluwa Osuntokun
52a9041180
Merge pull request #10713 from lightningnetwork/onion-msg-rate-limit
onionmessage+peer: rate-limit incoming onion messages per-peer and globally
2026-04-15 16:27:22 -07:00
Olaoluwa Osuntokun
8e91bcc463 docs: add release note for onion message rate limiting (#10713)
Add a single consolidated release note describing the onion-message
rate-limiting feature introduced earlier in this branch: the per-peer
and global byte-denominated token-bucket limiters, their defaults and
the four tunable flags, the 0/0 disable rule and the startup-time
rejection of invalid combinations, the channel-presence gate that
drops ingress from peers with no fully open channel, and the
protocol.onion-msg-relay-all opt-out for operators who want to accept
traffic from channel-less peers. Trimmed to the operator-facing
essentials; longer-form prose about the adversary model, the layers,
default sizing, and operator recipes lives in
docs/onion_message_rate_limiting.md, which the note links to.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
fc1ac5c638 docs: add onion message rate limiting explainer
Introduce docs/onion_message_rate_limiting.md, a prose explainer for
operators and contributors that covers the two-layer defense on the
onion message ingress path: the channel-presence gate that turns peer
identity into a capital cost, and the byte-denominated per-peer and
global token-bucket rate limiters that run behind it. The doc walks
through the adversary first so that each layer has a concrete thing
to defend against, then covers the knobs, the startup-time validation
rules, the default sizing, and the protocol.onion-msg-relay-all
escape hatch with its explicit tradeoff against Sybil resistance. A
short operator recipes section collects the common "I want to ..."
configurations so readers do not have to reconstruct them from the
principles.

No code change; documentation only.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
335b75981f lncfg+peer+server: add protocol.onion-msg-relay-all to bypass channel gate
Add a new protocol option, protocol.onion-msg-relay-all, that controls
whether incoming onion messages are required to come from peers with a
fully open channel. The default is false, which preserves the existing
behavior: the channel-presence gate drops messages from peers with no
channel before the rate limiters are consulted, so a new no-cost
identity cannot burn any per-peer byte budget and saturate the global
bucket. Setting the flag to true skips the gate so that onion messages
from any peer are admitted into the per-peer + global IngressLimiter
pipeline.

The flag is plumbed through ProtocolOptions in both the default and
integration build variants of lncfg/protocol*.go, threaded into the
peer subsystem as peer.Config.OnionRelayAll, and wired by the server
from s.cfg.ProtocolOptions.OnionMsgRelayAll alongside the existing
OnionLimiter field. allowOnionMessage gains a relayAll bool parameter;
the gate check becomes "if \!relayAll && \!hasChannel { drop }" so the
semantics of hasChannel stay pure — it still means "this peer has a
channel" — and the policy toggle lives entirely in the caller's
configuration rather than being spread across gate-state and flag
state.

sample-lnd.conf gains a commented-out entry for the new option with
the default value and an operator-facing note that enabling it trades
the Sybil-resistance property of the gate for reachability to peers
with whom we have no channel.

A new TestAllowOnionMessageRelayAll unit test exercises the four
(hasChannel, relayAll) combinations at the helper level, including
the key new behavior — a peer with hasChannel=false being rejected
under relayAll=false and admitted into the limiter under
relayAll=true — and the nil-limiter path under relayAll=true, which
must still accept. The existing allowOnionMessage tests were
extended with the new parameter set to false so they continue to
assert the gate semantics unchanged.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
c0827e8e39 peer: gate onion message ingress on having an open channel
Onion message forwarding is an unpaid side channel. Without any peer
qualification the byte-bucket limiters added in the previous commits are
our only defense against a Sybil attacker: an attacker that can cheaply
spin up N identities and burn a full per-peer byte budget on each one
saturates the global bucket and converts the aggregate cap into a
service-denial primitive against legitimate channel peers. This was
raised on PR review — the per-peer cap is good, but the global cap on
its own is a Sybil multiplier if peer identity is free. The proper fix
is to make new identities cost real capital, which is what requiring a
funded channel does.

This commit adds a channel-presence gate as the first check in
allowOnionMessage, ahead of both the per-peer and the global rate
limiters. Messages from peers that do not have at least one fully
open channel with us are dropped with a new dropReasonNoChannel
sentinel and never allocate any rate limiter state — the gate runs
before either limiter is consulted, so no-channel peers cannot burn
tokens on any bucket. Pending channels are deliberately excluded from
the check: they are represented as nil values in the activeChannels
map, are cheap to open and prone to getting stuck, and so do not
provide the capital-cost guarantee the Sybil defense depends on.
Existing Brontide cleanup paths (StopOnionActorIfExists,
OnionPeerLimiter.Forget) already handle teardown on peer disconnect;
nothing new is needed there because the gate keeps no-channel peers
from ever allocating per-peer state in the first place.

For the hot path we cannot afford to iterate the activeChannels
registry on every incoming onion message, so Brontide now carries a
numActiveChans atomic.Int32 that shadows the count of non-pending
entries in activeChannels. hasActiveChannels is a single atomic Load
and is therefore O(1). The counter is maintained in lockstep with
activeChannels at every mutation site: loadActiveChannels increments
it as it populates the registry during Start(); addActiveChannel uses
a new lnutils.SyncMap.Swap method (a thin typed wrapper around
sync.Map.Swap) to atomically replace any prior entry so that both
brand-new channels and pending-to-active promotions bump the counter
by exactly one; WipeChannel and handleRemovePendingChannel both use
LoadAndDelete so they can inspect the prior value and only decrement
when the removed entry was non-nil. Under race, this keeps the
counter and the map consistent even when RPC WipeChannel races with
the channelManager goroutine.

The accompanying unit tests cover: the no-channel drop path at the
allowOnionMessage level, asserting that neither the global stub
counter nor the per-peer limiter's dropped counter move when the
gate fires; the subsequent channel-gained path on the same peer,
asserting the same message is accepted once hasChannel flips; and a
focused Brontide-level test that walks the counter through initial
emptiness, a pending-only state (counter must stay at zero), a
pending-to-active promotion via direct Store + Add, the pending
delete path through handleRemovePendingChannel (must not underflow),
and the active delete path through LoadAndDelete + Add(-1) that
WipeChannel uses internally. Running with -race confirms the
Swap/LoadAndDelete patterns keep the counter and the map in sync
under concurrent access.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
1db6eb66a2 server+docs: construct onion message rate limiters and document config
With the limiter primitives, config options, and peer-side enforcement
in place, this commit constructs the per-peer and global onion message
rate limiters inside the server's onion messaging setup block,
composes them with NewIngressLimiter, and threads the resulting
IngressLimiter into peer.Config alongside the existing SpawnOnionActor
factory as a single OnionLimiter field. The limiters are only built
when onion messaging is actually enabled so that the disabled path
allocates nothing.

sample-lnd.conf gains commented-out entries for the four new options
with the default values and a short explanation of the ~5 Mbps
worst-case target. The user-facing release note for this feature is
added in a single consolidated commit at the end of the series rather
than split across the commits that introduce it.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
9cad57bfce peer: enforce onion message rate limits at ingress
This commit plumbs the combined IngressLimiter (per-peer + global)
through peer.Config and consults it from the readHandler's
*lnwire.OnionMessage case. The decision is factored into a small
allowOnionMessage helper so that the ingress policy is directly
unit-testable without standing up a full Brontide harness. Per-peer is
checked first inside the IngressLimiter: if we consulted the global
limiter first, a peer whose own bucket was already empty would still
get to burn a global token on each attempt, letting a single hostile
peer drain the shared budget and starve legitimate peers.

peer.Config carries a single OnionLimiter field of IngressLimiter type;
the brontide readHandler calls a single AllowN per incoming onion
message and dispatches on sentinel errors via errors.Is for the
first-drop log path. Nil limiter values are treated as "disabled"
throughout, which both preserves the pre-change behavior when onion
messaging is entirely turned off and keeps the brontide test harness
from needing to construct real limiters. Per-peer bucket state is
retained across disconnect at the IngressLimiter layer so a peer
cannot cycle the connection to reset its per-peer allowance.

OnionMessage also gains a WireSize method that computes the
on-the-wire size directly from the in-memory fields (no round-trip
through Encode) so the hot ingress path can charge the right number of
byte tokens without paying for a full serialization.

The accompanying unit tests cover the nil/disabled path, the
per-peer-rejects-first ordering invariant (asserting the global
limiter is not consulted when the per-peer bucket is empty), the
global rejection path, per-peer isolation across distinct pubkeys, and
a small concurrent stress test that asserts every attempt is accounted
for as either accepted or dropped and that the total accepted count
equals the configured burst under -race. A property-based rapid test
on WireSize guards against silent divergence from WriteMessage if the
OnionMessage wire format ever gains a TLV extension.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
69468c3219 lncfg+config: add tunable onion message rate limit options
In this commit we surface the onion message rate limiter thresholds as
ProtocolOptions so that operators can tune them from lnd.conf or the
command line. Four options are added — onion-msg-peer-rate,
onion-msg-peer-burst, onion-msg-global-rate, and onion-msg-global-burst —
and are documented such that a rate of zero disables the corresponding
limiter entirely. The default values are seeded from the constants added
in the previous commit via DefaultConfig, following the same pattern that
the Gossip sub-config already uses for its own rate limiter knobs.

The fields are duplicated into protocol_integration.go so that the
integration build tag sees the same surface; this mirrors how the
existing NoOnionMessagesOption and related fields are declared.
2026-04-15 13:23:50 -07:00
Olaoluwa Osuntokun
d95bcbfa0b onionmessage: add token-bucket rate limiter primitives
The existing per-peer actor mailbox (capacity 50, RED from depth 40) only
bounds in-flight queue depth. It does not cap throughput: a peer that
drains its actor quickly can saturate our Sphinx unwrap CPU, replay-DB
writes, and outbound forwarding bandwidth without ever tripping RED. At
spec-max onion message sizes (~32 KiB per sphinx packet) a single
well-behaved-draining peer is enough to push multiple Mbps of unpaid
forwarded traffic through us, and aggregate fan-in from many peers
multiplies that into tens of Mbps — an amount of bandwidth that is very
much out of proportion for a side channel on a payment routing node.

This commit adds the building blocks for two token-bucket limiters that
will be wired into the onion message ingress path in a follow-up commit:
a process-wide global limiter and a per-peer registry. Both drop (rather
than wait) on over-limit so that a hostile peer cannot grow our
goroutine or memory footprint simply by sustaining above-threshold
traffic. The per-peer registry keys buckets on the peer's compressed
pubkey, creates them lazily, and retains them for the lifetime of the
process so a peer cannot reset its burst by cycling the connection;
cardinality is bounded by the live channel-peer count (the ingress call
site gates on having a channel before allocating per-peer state), so no
time-based GC is needed.

A minimal RateLimiter interface is introduced so that callers and tests
can substitute noop or alternate implementations without reaching into
x/time/rate directly, and a small countingLimiter wrapper keeps an
atomic drop counter plus a one-shot first-drop flag for observability.
A rate of zero (or a non-positive burst) yields a noop limiter,
providing a clean "disabled" mode without branching at the call site.

On top of those, a single IngressLimiter interface composes the
per-peer and global buckets behind one surface so that callers —
notably the peer readHandler — only thread one object through Config
and call one method per incoming onion message. Drop reasons are
surfaced as sentinel errors (ErrPeerRateLimit, ErrGlobalRateLimit)
wrapped in fn.Result[fn.Unit] so callers match on them with errors.Is
rather than comparing free-form strings. The stock implementation
encodes the load-bearing ordering — per-peer first, then global —
inside AllowN so that a hostile peer whose own bucket is already empty
cannot burn global tokens on every rejected attempt and starve
legitimate peers.

Default constants targeting roughly ~5 Mbps worst-case ingress at
spec-max message sizes are added alongside the existing mailbox
defaults.
2026-04-15 13:23:50 -07:00
ziggieXXX
10808eb2ee
Merge pull request #10628 from Roasbeef/funding-optimization
funding: add quick path for handling `channel_ready` messages
2026-04-15 16:07:49 +02:00
ziggieXXX
3fc6741614
Merge pull request #10730 from ziggie1984/itest/fix-introduction-blinded-error-flake
itest: fix flake in testIntroductionNodeError
2026-04-14 07:51:42 +02:00
ziggie
33cb63f33a
itest: fix flake in testIntroductionNodeError
`testRelayingBlindedError` already uses `flakePaymentStreamReturnEarly`
after draining Carol's outgoing liquidity for the same reason:
`drainCarolLiquidity` causes the draining node to originate a payment,
which produces SEND-type HTLC notifier events. Because `SendPaymentV2`
returns SUCCEEDED before the commitment dance (revoke-and-ack exchange)
completes, those events can still be in-flight when the test subscribes
to HTLC events. The htlc notifier's subscribe server races a pending
SendUpdate against the Subscribe call in a single handler goroutine; if
registration wins the random select, the stale SEND events land on the
new subscriber and corrupt the subsequent FORWARD-type assertion.

`testIntroductionNodeError` has the same pattern — Bob drains Carol's
incoming liquidity by originating a payment — but was missing the sleep.
Add `flakePaymentStreamReturnEarly()` to match the existing workaround.

Fixes: https://github.com/lightningnetwork/lnd/actions/runs/24278289474/job/70895848920
2026-04-14 06:32:44 +02:00
Olaoluwa Osuntokun
c68397b9bd funding/test: add test for inline channel_ready processing
In this commit, we add TestChannelReadyUnknownChannelID which verifies
that channel_ready messages with unrecognized ChannelIDs are processed
inline in the reservation coordinator without spawning goroutines. The
test sends 100 channel_ready messages with random ChannelIDs, waits for
all of them to be consumed (verified via a FindChannel call counter),
then asserts that the goroutine count hasn't grown proportionally. It
also confirms the coordinator remains responsive by successfully opening
a new channel after the batch completes.
2026-04-13 16:39:27 -07:00
Olaoluwa Osuntokun
312dda86c2 funding: process channel_ready messages inline in the coordinator
In this commit, we refactor how the funding manager handles incoming
channel_ready messages. Previously, every channel_ready message would
unconditionally spawn a new goroutine via `go f.handleChannelReady(...)`,
making it the only message type in the coordinator switch that wasn't
processed inline. We now handle channel_ready the same way as all the
other funding messages: synchronously within the reservation coordinator
loop.

The goroutine was originally needed because handleChannelReady may need
to block on a `localDiscoverySignal` while the channel's funding
confirmation flow completes locally. In this commit, we split the
function into two parts: `handleChannelReady` (the lightweight entry
point that runs inline) and `processChannelReady` (the extracted body
that does the actual DB lookup and channel finalization). The inline
entry point checks whether a `localDiscoverySignal` exists for the given
channel ID, and only in that case do we dispatch a goroutine to wait for
the signal before calling `processChannelReady`. For channels that have
already confirmed (or after a restart), no goroutine is spawned at all.

This short-circuits the common path: the `FindChannel` DB lookup and the
rest of the processing now happen inline in the coordinator for the
majority of channel_ready messages, reducing goroutine churn and keeping
the coordinator's message processing consistent across all message types.
2026-04-13 16:39:27 -07:00
Olaoluwa Osuntokun
ad303828cf
Merge pull request #9985 from Roasbeef/prod-taproot-chans
multi: implement awareness of the final/production taproot channel variant
2026-04-13 16:02:21 -07:00
Olaoluwa Osuntokun
85bba2ca7a multi: add SCB restore support for production taproot channels
Add the missing SimpleTaprootFinalVersion case to
chanrestore.openChannelShell() so that SCB backups created for
production taproot channels can be properly restored. Without this,
the channel type bits were not reconstructed during restore, causing
DLP to fail.

Also add integration tests for both confirmed and zero-conf variants
of production taproot channel backup restoration.
2026-04-13 12:22:16 -07:00
Olaoluwa Osuntokun
fd3b6386fe multi: fix lint and itest failures for production taproot channels
Fix line length lint violations in utxonursery.go by adding nolint:ll
directives to long case statements for production taproot witness types.

Fix itest funding negotiation test to handle SIMPLE_TAPROOT_FINAL in
the taproot negotiation failure check. Previously the test only expected
failure when Carol wanted SIMPLE_TAPROOT and Dave lacked taproot
support, but did not handle the symmetric case where Carol wants
SIMPLE_TAPROOT_FINAL.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
98c086ba5d chanbackup: add SimpleTaprootFinalVersion for production taproot backups
Add a dedicated backup version (7) for production taproot channels that
use final scripts with OP_CHECKSIGVERIFY. This distinguishes them from
staging taproot channels in the SCB format, ensuring backup
compatibility is explicit about the channel type.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
aaf7c29423 lnwallet: return error from AggregateNonces in MusigSession
Fix swallowed error in MusigSession.Refresh where AggregateNonces
failure returned nil instead of the actual error.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
91e35c4d5a docs/release-notes: add release note for production taproot channels 2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
890636a131 multi: fix linter issues
Fix gci (extra blank lines) and ll (line length > 80 chars) issues
across contractcourt, input, and watchtower packages.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
086f692778 itest: extend watchtower breach test to cover production taproot channels
Add SIMPLE_TAPROOT_FINAL to the watchtower revoked close retribution
test matrix. This exercises the new FlagTaprootFinalChannel blob type
and ensures the watchtower correctly constructs justice transactions
using production taproot scripts with OP_CHECKSIGVERIFY.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
7a18fba663 watchtower: add production taproot channel support to justice kit
Wire channel type through BreachRetribution and the watchtower blob
system to support production taproot channels with final scripts.

The key changes are:

1. Add ChanType field to BreachRetribution so downstream consumers
   (including the watchtower) can determine the script variant.

2. Add FlagTaprootFinalChannel blob type flag and
   TypeAltruistTaprootFinalCommit blob type to distinguish production
   from staging taproot channels in watchtower backups.

3. Add TaprootFinalCommitment to the watchtower's CommitmentType enum
   with appropriate witness type and size mappings.

4. Update taprootJusticeKit to use WithProdScripts() when constructing
   script trees for production taproot channels. The isFinal flag is
   set during construction from BreachRetribution.ChanType and during
   deserialization from the blob's commitment type.

Without this change, the watchtower would construct justice transactions
using staging scripts for production taproot channels, resulting in
invalid witnesses that fail to sweep breached outputs.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
65d04f3461 multi: fix linter issues 2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
a279700481 lnwallet: fix fundingTxid scope in TestChanSyncTaprootLocalNonces 2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
1d7b5bbe44 lnrpc: regenerate protobuf files for Go 1.26 compatibility 2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
1866770f43 lnwallet: regenerate test vectors with BIP-340 HTLC signatures
Regenerate the test vector JSON with HTLC second-level transaction
signatures that use BIP-340 standard nonce derivation (zero auxrand)
instead of RFC6979. This makes the HTLC signatures reproducible across
different Schnorr implementations. The commitment transaction MuSig2
signatures are unchanged.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
63450b85ff lnwallet: use BIP-340 nonce derivation for HTLC sigs in test vectors
btcd's schnorr.Sign defaults to RFC6979 nonce derivation, while
libsecp256k1 (used by eclair, CLN, etc) uses BIP-340's standard
nonce derivation with zero auxrand. Both are deterministic but produce
different signatures for the same key and message, causing HTLC
signature mismatches in interop test vectors.

This commit introduces a bip340Signer wrapper that overrides
SignOutputRaw for taproot script path spends to use
schnorr.CustomNonce([32]byte{}) — matching BIP-340 deterministic
signing behavior. The wrapper is only used in the test vector
generator; production signing paths are unchanged.

Note that MuSig2 commitment signatures were already using BIP-340
nonces internally (via the musig2.Sign path), so only the HTLC
second-level transaction signatures were affected.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
50981dfce1 lnwallet: regenerate taproot test vectors with secret nonces
Regenerate the test vectors JSON to include local_sec_nonce and
remote_sec_nonce fields alongside the existing public nonces. The local
nonce fields now correctly correspond to local's verification nonce for
their own commitment transaction, matching the commitment tx stored in
the test vector.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
4c225ddf33 lnwallet: add MuSig2 secret nonces and partial sig replay to test vectors
In this commit, we extend the taproot test vector generator and verifier
to include MuSig2 secret nonces and a full partial signature replay
test.

For the generator, we now capture the correct nonces for each
commitment transaction: local's verification nonce (from LocalSession)
for local's own commitment, and remote's JIT signing nonce (from
RemoteSession) for the same commitment. Previously, the local nonce was
incorrectly captured from the RemoteSession, which corresponds to a
different commitment transaction.

The new musig2_partial_sig_replay test sub-suite verifies three
properties for each test case:

 1. The remote partial sig can be independently reproduced from the
    secret nonce and private key using musig2.Sign().

 2. The local partial sig can be independently produced and verified
    using the local secret nonce.

 3. Both partial sigs combine (via the Session API) into the exact
    Schnorr signature present in the commitment transaction witness.

This enables interop implementations to validate their MuSig2 signing
logic against the test vectors without needing to match nonce derivation
algorithms across different secp256k1 libraries.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
2148445c6b lnwallet: add secret nonce stashing to MusigSession for test vectors
In this commit, we add the ability for MusigSession to capture and
expose the raw 97-byte MuSig2 secret nonce generated during JIT signing
nonce creation. This is gated behind the customNonceRand option, so it
only activates in test vector generation mode.

The stashed secret nonce is consumed on read (cleared after access) to
prevent accidental nonce reuse. This enables interop test vectors to
include the raw secret nonces, allowing other implementations to replay
the MuSig2 signing process without needing to match the exact nonce
derivation algorithm used by btcd's musig2 library.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
70f189ffcb lnwallet: regenerate taproot channel test vectors
Regenerate `test_vectors_taproot.json` to reflect the corrected test
vector generator. Changes include actual 32-byte MuSig2 partial
signatures (replacing the dummy 8-byte DER stubs), 66-byte public
nonces for both local and remote parties, corrected HTLC sig-to-
transaction mapping sorted by BIP 69 output index, proper HTLC-success
witness layout with preimage in the correct witness slot, and the
updated trimming test case which now trims 3 of the 5 test HTLCs below
the 2500 sat dust limit (down from 5 HTLC outputs to 2).
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
77da917c8e lnwallet: add 3rd-party signature verification for taproot test vectors
In this commit, we add a `signature_verification` sub-test to the
taproot test vector verifier that performs full script execution against
both the commitment transaction and all HTLC resolution transactions.

This uses `txscript.NewEngine` to execute the taproot witness programs
exactly as a Bitcoin node would, providing an independent check that all
signatures in the test vectors are cryptographically valid. For the
commitment transaction, we verify its witness against the funding output
pkScript. For each HTLC resolution transaction, we verify its witness
against the corresponding commitment output it spends.

This catches issues that the structural comparison tests (hex matching)
cannot: for instance, a transaction can have the correct structure but
carry an invalid signature if the sighash was computed over the wrong
prevout or if the wrong key was used for signing. Running the full
script engine also validates the control block, the tap leaf hash, and
the overall taproot spend path.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
745bdc1897 lnwallet: fix HTLC trimming test case to use dust_limit for zero-fee HTLCs
In this commit, we fix the "commitment tx with some HTLCs trimmed" test
case to actually exercise trimming for taproot's zero-fee HTLC
transactions.

With zero-fee second-level HTLCs, the HTLC output value on the
commitment transaction equals the HTLC amount directly (no fee is
deducted). This means trimming is determined solely by whether the HTLC
amount falls below the dust limit, not by the fee rate. The previous
parameters (fee_per_kw=100000, dust_limit=546) didn't actually trim any
of the test HTLCs because even the smallest test HTLC (1000 sats) was
above the 546 sat dust limit.

We now use fee_per_kw=644 (a reasonable rate) and dust_limit=2500 to
ensure that the three smallest test HTLCs (1000, 2000, 2000 sats) are
properly trimmed, leaving only the 3000 and 4000 sat HTLCs on the
commitment transaction.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
b78de44d11 lnwallet: fix HTLC sig-to-transaction mapping in test vector generator
In this commit, we fix two interrelated bugs in the way HTLC signatures
are associated with their corresponding second-level transactions in the
taproot test vector generator.

The first issue was that HtlcSigs are sorted by BIP 69 output index
(matching the commitment transaction's output ordering), but the old
code was assigning signatures using the iteration order of incoming
HTLCs followed by outgoing HTLCs. This meant timeout transaction
signatures were getting paired with success transactions and vice versa
whenever the output ordering didn't happen to match the incoming-first
iteration order. This is the root cause of the invalid HTLC-timeout
signatures that eclair reported when cross-validating.

We now collect all HTLC entries (both incoming and outgoing) into a
single slice, sort them by their commitment output index, then zip them
against the HtlcSigs array so each signature lines up with the correct
second-level transaction.

The second issue was in the HTLC-success preimage extraction path. The
old code read the witness script from index [4] (the control block) and
used a hardcoded byte offset of 69 to locate the payment hash, then
wrote the preimage into index [3] (overwriting the script). The correct
taproot witness layout is [remoteSig, localSig, preimage, script,
controlBlock], so the script lives at [3] and the preimage slot is [2].
We now use `txscript.ScriptTokenizer` to walk the script opcodes and
find OP_HASH160 followed by the 20-byte push data, which is far more
robust than relying on fragile byte offsets that break if the script
template ever changes.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
fa97946ff2 lnwallet: emit actual MuSig2 partial sigs and nonces in test vectors
In this commit, we fix the taproot test vector generator to capture and
emit the real MuSig2 partial signatures and public nonces rather than
the dummy `CommitSig` value which is zeroed out for taproot channels.

Previously, the generator was reading from `CommitSig.ToSignatureBytes()`
which yielded a minimal DER encoding of `(0, 0)` (the 8-byte string
`3006020100020100`). For taproot channels the actual signature lives in
the `PartialSig` field of the `CommitSigs` struct, which carries both
the 32-byte partial sig scalar and the 66-byte compressed public nonce
needed by the verifier to reconstruct the combined signature.

We now unwrap the `PartialSig` from both the local and remote commitment
signatures, extract the nonce and sig bytes, and include `local_nonce`
and `remote_nonce` fields alongside `remote_partial_sig` in the emitted
JSON. This gives other implementations (eclair, CLN, etc.) all the
material they need to independently verify commitment transaction
signatures using their own MuSig2 libraries.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
38c415a924 lnwallet: add taproot channel test vector generator
In this commit, we add a test vector generator and verifier for
taproot channel constructions. All vectors are derived
deterministically from a single 32-byte seed using SHA256(seed ||
label) for key derivation, ensuring any implementation can reproduce
them independently.

The generator covers two areas:

Script vectors decompose the full tapscript trees for every output
type: funding (MuSig2 aggregated key), to_local (delay + revocation
leaves), to_remote (1-block CSV leaf), anchors (OP_16 OP_CSV),
offered/accepted HTLCs on both local and remote commits, and
second-level HTLC transactions. Each entry captures the raw leaf
scripts, leaf hashes, tapscript root, internal key, output key, and
pkScript.

Transaction vectors produce full serialized commitment transactions
and HTLC resolution transactions for three scenarios: a simple
commitment with no HTLCs, a commitment with five untrimmed HTLCs,
and the same HTLCs at a higher fee rate causing some to be trimmed.

To generate: go test -run TestTaprootVectors ./lnwallet/ -args -generate-taproot-vectors
To verify:   go test -run TestTaprootVectors ./lnwallet/
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
08c42b19da multi: add custom nonce rand support to MuSig2 sessions
In this commit, we add the ability to inject a custom random source
for generating JIT (Just-In-Time) signing nonces in MuSig2 sessions.
By default, MuSig2 signing nonces are generated using crypto/rand,
which makes signatures non-deterministic across runs. For test vector
generation, we need fully reproducible signatures from a fixed seed.

A new `customNonceRand` field is threaded through `MusigSession`,
`MusigSessionCfg`, `MusigPairSession`, and exposed via the
`WithCustomSigningRand` channel option. When set, the custom reader
is passed to `musig2.WithCustomRand()` during JIT nonce generation
in `SignCommit`. All existing callers pass `fn.None[io.Reader]()` to
preserve the current behavior of using the system CSPRNG.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
29de2c8618 cmd/commands: add taproot-final to lncli open command 2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
5ffcd823ef multi: use feature bits to pick which taproot nonce field to use
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.
2026-04-13 12:21:42 -07:00
Olaoluwa Osuntokun
55ab4b03d0 lnwallet: add support for local nonces map in revoke_and_ack
We only ever set a single nonce, but we'll check both fields to make
sure that at least one of them is set.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
09acceda99 lnwire: add local_nonces field to revoke_and_ack 2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
5bbfdc6267 itest: extend relevant itests to cover taproot chans final 2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
d86b84001f lnrpc/walletrpc: add witness types for taproot chans final 2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
597af82ff8 watchtower: prepare infrastructure for production taproot support
This commit adds preparatory infrastructure and TODO markers in the watchtower
justice kit generation for future production taproot channel support. The
changes establish placeholders for channel type detection and script option
selection that will be needed when watchtowers begin handling breach scenarios
for production taproot channels.

The current implementation includes commented code structures that demonstrate
the intended approach for integrating production script options into the
justice transaction generation process. When channel type information becomes
available in the BreachRetribution structure, these placeholders can be
activated to ensure that watchtowers generate justice transactions using the
appropriate script optimization level.

This preparatory work ensures that the watchtower system has a clear path
toward production taproot support while maintaining current functionality
for staging taproot and legacy channels. The TODO comments provide explicit
guidance for future development when watchtower breach handling for production
taproot channels is implemented.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
d15b9616b7 itest+input: add production taproot channel integration tests
This commit adds comprehensive integration test coverage for production
taproot channels to validate end-to-end functionality in realistic scenarios.
The tests verify that production taproot channels can be successfully opened,
operated, and closed using the finalized taproot specification with optimized
scripts and feature bits 80/81.

The integration tests cover channel opening with the SIMPLE_TAPROOT_FINAL
commitment type, ensuring that the complete channel lifecycle works correctly
with production taproot features. Test utilities have been enhanced to support
production taproot channel creation and validation, providing the necessary
infrastructure for comprehensive testing scenarios.

Additional unit tests have been added to the input package to validate size
calculations and witness generation for production taproot witness types.
These tests ensure that the new Final witness types produce correctly sized
witnesses and transactions, maintaining the expected efficiency benefits of
the optimized script structure.

The test coverage helps ensure that production taproot channels operate
correctly across all system components while maintaining compatibility with
existing channel types and providing confidence in the production readiness
of the implementation.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
27ae0cb598 funding: add production taproot channel negotiation support
This commit extends the funding manager's commitment type negotiation logic
to handle production taproot channels. The negotiation system now recognizes
and properly processes requests for channels using the final taproot
specification with feature bits 80/81 and optimized script structures.

The commitment type negotiation has been enhanced to distinguish between
staging and production taproot variants during the channel opening process.
When a production taproot channel is requested, the negotiation logic ensures
that both parties support the necessary feature bits and applies the
appropriate channel type configuration including the TaprootFinalBit flag.

Comprehensive test coverage has been added to validate the negotiation
behavior for production taproot channels, ensuring that the funding process
correctly handles feature bit validation, commitment type mapping, and error
conditions. The tests verify that production taproot channels are only
established when both peers indicate support for the finalized taproot
features.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
4ee8bd58f9 lnrpc+rpcserver: add production taproot commitment type to RPC interface
This commit extends the Lightning RPC interface to support production taproot
channels by adding a new SIMPLE_TAPROOT_FINAL commitment type. This allows
external clients to explicitly request channels that use the finalized taproot
specification with optimized script structures and feature bits 80/81.

The RPC server has been updated to properly handle the new commitment type
during channel opening operations, mapping the SIMPLE_TAPROOT_FINAL type to
the appropriate internal channel type flags including both SimpleTaprootFeatureBit
and TaprootFinalBit. This ensures that channels opened through the RPC interface
are properly configured with production taproot capabilities.

The existing SIMPLE_TAPROOT commitment type has been clarified in its
documentation to indicate that it represents the staging version using
development scripts, providing clear distinction between the two taproot
variants available to RPC clients. The protobuf definitions and generated
code have been updated accordingly to support this new functionality.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
2e0343cc00 lnwallet: integrate production script options in commitment generation
This commit updates the wallet's commitment transaction generation logic to
use appropriate script options based on the channel type. The commitment
builder now determines whether a channel uses production taproot scripts
and passes the WithProdScripts() option accordingly to HTLC script generation
functions.

The changes affect three key areas of the wallet: channel state management,
commitment transaction construction, and funding reservation handling. Each
area now properly detects production taproot channels using the IsTaprootFinal()
method and applies the correct script generation options to ensure consistency
with the channel's script optimization level.

This integration ensures that production taproot channels generate commitment
transactions with optimized script trees using OP_CHECKSIGVERIFY, while
maintaining full compatibility with staging taproot and legacy channel types.
The script option selection is applied consistently across all commitment
transaction scenarios including local commits, remote commits, and HTLC
processing.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
60b3d2c99e input: thread script options through taproot HTLC functions
This commit extends the taproot HTLC script generation functions to accept
TaprootScriptOpt parameters, enabling callers to specify whether production
or staging script variants should be generated. The SenderHTLCScriptTaproot
and ReceiverHTLCScriptTaproot functions now accept a variadic opts parameter
that is forwarded to the underlying script tree construction.

This change provides the necessary infrastructure for the wallet and contract
resolution systems to generate the appropriate script trees based on channel
type. Production taproot channels can now pass the WithProdScripts() option
to generate optimized scripts using OP_CHECKSIGVERIFY, while staging channels
continue to use the existing development script structure.

The modification maintains backward compatibility by making the opts parameter
variadic with sensible defaults. Existing callers that do not specify options
will continue to generate staging scripts as before, ensuring no disruption
to current functionality while enabling future production script support.
2026-04-13 12:17:56 -07:00
Olaoluwa Osuntokun
f9a8598639 contractcourt: integrate production taproot support in nursery
This commit completes the production taproot integration by updating the
UTXO nursery to properly handle production taproot channels. The nursery
is responsible for incubating time-locked outputs from commitment transactions
and must use the correct witness types for successful sweeping operations.

The witness type selection logic has been updated in three key areas within
the IncubateOutputs function: incoming HTLC resolution handling, outgoing
HTLC resolution handling, and baby output creation through makeBabyOutput.
Each location now uses a consistent three-way selection pattern that chooses
production taproot witness types for final channels, staging types for
development channels, and legacy types for traditional channels.

A new helper method isProdTaprootResolution has been added to determine
production taproot channels by examining the presence of a ResolutionBlob,
which indicates auxiliary channel information used by production taproot
implementations. The makeBabyOutput function has been converted to a method
to access this helper function.

The NurseryReport function has been updated to include all new Final witness
types in its switch statements, ensuring that production taproot outputs are
properly categorized and reported. This maintains consistency in the nursery's
reporting system while supporting the new witness types.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
269d2c382e contractcourt: implement production taproot witness selection
This commit implements the core logic for selecting appropriate witness types
based on channel type in the contract resolution system. The commit sweep
resolver, HTLC timeout resolver, and HTLC success resolver have been updated
to use production taproot witness types when handling final taproot channels.

For each resolver, the witness type selection follows a consistent three-way
pattern: production taproot channels use Final witness types, staging taproot
channels use the existing taproot witness types, and legacy channels continue
to use their established witness types. This ensures that each channel type
uses the appropriate script structure and witness generation logic.

The HTLC success resolver required the addition of a new production input
constructor to properly handle direct HTLC sweeps on remote commitments with
production taproot channels. The HTLC timeout resolver was updated to handle
both second-level timeout transactions and direct timeout sweeps with the
correct production witness types.

These changes ensure that production taproot channels benefit from the
optimized script structure using OP_CHECKSIGVERIFY while maintaining full
backward compatibility with staging taproot and legacy channel types.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
6b0c3f5c11 contractcourt: add channel type support to HTLC resolvers
This commit extends all HTLC contract resolvers to accept and store channel
type information, which is essential for determining whether to use staging
or production taproot witness types during contract resolution. Each resolver
constructor now accepts a channeldb.ChannelType parameter and stores it as
a field within the resolver struct.

The channel arbitrator has been updated to extract channel type information
from the channel state and pass it to all resolver constructors. This ensures
that each resolver has the necessary context to make appropriate decisions
about script generation and witness type selection based on the specific
channel type being resolved.

Helper methods isTaprootFinal() have been added to each resolver to provide
a clean interface for determining when production taproot witness types
should be used. This lays the groundwork for the resolvers to properly
handle both staging and production taproot channels with the correct
script optimizations.

The changes maintain backward compatibility with existing channel types
while providing the infrastructure needed for production taproot channel
support throughout the contract resolution system.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
749a8e4555 channeldb: add production taproot channel type support
This commit introduces the infrastructure necessary to distinguish between
staging and production taproot channels in the channel database. A new
TaprootFinalBit flag is added to the ChannelType enumeration to identify
channels that use the final taproot specification with optimized scripts.

The IsTaprootFinal() method provides a clean interface for determining when
a channel uses production taproot scripts versus the staging implementation.
Production taproot channels are characterized by their use of feature bits
80/81 and optimized script structures that employ OP_CHECKSIGVERIFY for
improved efficiency and reduced transaction sizes.

This channel type distinction is essential for the contract resolution system
to select appropriate witness types and script generation options. The bit
must be set alongside SimpleTaprootFeatureBit to ensure proper channel type
validation and backwards compatibility with existing taproot implementations.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
0720abca39 input: add production taproot HTLC succeed input constructor
This commit adds MakeTaprootHtlcSucceedInputFinal, a new input constructor
specifically for creating HTLC success inputs that use production taproot
witness types. This function parallels the existing MakeTaprootHtlcSucceedInput
but creates inputs with the TaprootHtlcAcceptedRemoteSuccessFinal witness type
instead of the staging variant.

The new constructor follows the same pattern and signature as its staging
counterpart, ensuring consistency in the input creation API. This allows
contract resolvers to create the appropriate input type based on whether
they are handling a staging or production taproot channel, ensuring that
the correct witness generation logic is applied during transaction creation.

This addition provides the necessary infrastructure for production taproot
channels to properly construct inputs for sweeping HTLC outputs on remote
commitment transactions with the optimized script structure.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
fc2e01a326 input: add production taproot witness types for final channels
This commit introduces seven new witness types specifically designed for
production taproot channels that use the final optimized script structure.
These witness types correspond to the existing staging taproot witness types
but are intended for channels using the finalized taproot specification with
optimized scripts that employ OP_CHECKSIGVERIFY instead of OP_CHECKSIG + OP_DROP.

The new witness types cover all taproot channel operations including local and
remote commitment spends, second-level HTLC transactions, direct HTLC sweeps,
and revocation scenarios. Each production witness type follows the established
naming convention by appending "Final" to distinguish them from their staging
counterparts.

The witness generation logic for these new types mirrors the existing taproot
implementation but will be used when the channel type indicates a production
taproot channel rather than a staging one. This ensures that the correct
script tree structure and witness format is used for each channel type.
2026-04-13 12:17:55 -07:00
Olaoluwa Osuntokun
8ce6327c5e feature: add taproot final to feature bit manager 2026-04-10 19:41:48 -07:00
Olaoluwa Osuntokun
6b53079bdd
Merge pull request #10728 from gijswijs/fix-syncer-test-err-check
discovery/test: assert ProcessSyncTransition error in sync transition test
2026-04-10 19:24:12 -07:00
Olaoluwa Osuntokun
48e3a2f940 build: update to actor v0.0.6 2026-04-10 19:21:53 -07:00
Gijs van Dam
cc375a6642 discovery/test: assert ProcessSyncTransition error in sync transition test
TestGossipSyncerSyncTransitions calls ProcessSyncTransition but
discards its return value. If the function ever regresses and returns
an error on the happy path, the test would silently pass. Assert
that the returned error is nil.
2026-04-10 19:20:59 -07:00
Olaoluwa Osuntokun
a54a4d4e50
Merge pull request #10589 from Roasbeef/discovery-errchan-futs
discovery: replace chan error w/ actor.Promise[error] for gossip results
2026-04-10 19:19:31 -07:00
Olaoluwa Osuntokun
fb86988c4e actor/test: extend TestAwaitFuture to cover fn.Err result path
In this commit, we extend TestAwaitFuture to cover the case where the
future is completed with an fn.Err result. The existing test only
exercised the fn.Ok (success) and context cancellation paths.

The new case calls promise.Complete(fn.Err[string](sentinel)) directly
and verifies that AwaitFuture surfaces the error as the second return
value while returning the zero string value in the first, which is the
documented contract for Result[T].Unpack().
2026-04-10 19:16:49 -07:00
Olaoluwa Osuntokun
532a7032d6 multi/test: add unit tests for gossip result helpers and context bridge
In this commit, we add test coverage for the new helper functions
introduced as part of the chan error -> Future[error] migration.

discovery/gossip_result_test.go covers AwaitGossipResult (success,
error propagation, and context cancellation) and the idempotency of
completeGossipResult (a second call must never block or overwrite the
first result).

lnutils/context_test.go covers ContextFromQuit, verifying that closing
the quit channel cancels the derived context and that calling cancel()
allows the internal goroutine to exit cleanly without leaking.

actor/future_test.go removes a stale doc comment that was left over
from a prior edit pass.
2026-04-10 19:16:49 -07:00
Olaoluwa Osuntokun
ac71ea7559 discovery+funding+peer+server: migrate gossip result to actor.Future[error]
In this commit, we eliminate the three buffered chan error patterns in
the discovery package and replace them with actor.Promise[error]/
actor.Future[error]. The old pattern is error-prone: if a channel is
completed more than once (e.g. when a deferred message copy is
re-enqueued and processed again), the second write to a capacity-1
channel blocks forever. actor.Promise.Complete() is idempotent via
sync.Once, so the second call is always a safe no-op regardless of
whether anyone holds a reference to the Future.

Additionally, PropagateChanPolicyUpdate previously blocked on <-errChan
after enqueuing a policy update with no quit-channel check, creating a
latent deadlock if the gossiper shut down between enqueue and send. It
now uses AwaitGossipResult with a ContextFromQuit-derived context, so
shutdown is always respected.

This is an atomic migration that updates all callers in the same
commit so each commit builds standalone. The three main pieces are:

discovery

networkMsg.err chan error becomes errPromise actor.Promise[error].
chanPolicyUpdateRequest.errChan chan error becomes errPromise.
syncTransitionReq.errChan chan error becomes errPromise. All ~65 sites
that previously wrote to the error channel now call
completeGossipResult(nMsg.errPromise, err) instead.

ProcessRemoteAnnouncement and ProcessLocalAnnouncement now return
actor.Future[error] instead of chan error. The capacity-2 buffer
comment on ProcessRemoteAnnouncement, which was itself a workaround
for the old pattern, is removed along with the TODO referencing the
actor model redesign. ProcessSyncTransition in syncer.go follows the
same pattern: the errChan select is replaced with AwaitGossipResult
using a ContextFromQuit-derived context.

funding

The SendAnnouncement function type in funding.Config changes from
returning chan error to returning actor.Future[error]. The call sites
in addToGraph and announceChannel are updated to await the future with
AwaitGossipResult, passing a context derived from f.quit via
ContextFromQuit. Shutdown signals (context.Canceled and
discovery.ErrGossiperShuttingDown) are both mapped to
ErrFundingManagerShuttingDown via the new mapGossipError helper, which
also factors out the duplicated graph-rejected / unknown-error
handling. The three mock SendAnnouncement implementations in
manager_test.go are updated accordingly.

peer+server

In peer/brontide.go, the ProcessRemoteAnnouncement call in the gossip
stream handler intentionally discards the result since remote gossip
messages are fire-and-forget from the peer's perspective. The old
comment explaining why the chan error was unsafe to use is replaced
with a note that an unawaited Future[error] carries no overhead.

In server.go (applyChannelUpdate), the previous select on errChan and
the quit channel is replaced with ContextFromQuit + AwaitGossipResult.
2026-04-10 19:16:49 -07:00
Olaoluwa Osuntokun
fc348602fe discovery: add gossip result helpers wrapping actor.Future[error]
In this commit, we add gossip_result.go with three thin wrappers that
form the internal vocabulary for the chan error -> Future[error]
migration.

completeGossipResult(p Promise[error], err error) resolves a gossip
processing promise. A nil error signals success; non-nil signals the
specific gossip failure. Calling it more than once is safe since the
underlying Promise.Complete uses sync.Once, making any repeat call a
no-op. This idempotency is the core property that makes the pattern
safe for deferred message re-processing.

AwaitGossipResult(ctx, f Future[error]) error is the public-facing
counterpart: it blocks until the future resolves or the context is
cancelled, returning whichever error applies. Callers outside the
discovery package (funding, server) use this.

awaitGossipResult is a package-internal alias for AwaitGossipResult,
avoiding the need to qualify the symbol inside the package.
2026-04-10 19:16:49 -07:00
Olaoluwa Osuntokun
eadea00aeb lnutils: add ContextFromQuit to bridge quit channels to contexts
In this commit, we add ContextFromQuit, a utility for bridging the quit
channel shutdown pattern to context.Context-based cancellation.

Several subsystems in lnd use a plain quit <-chan struct{} for
cooperative shutdown rather than a context.Context. When those
subsystems need to await a Future[error], which uses a context for
cancellation, they need a way to derive a context that is cancelled
when the quit channel closes.

ContextFromQuit does exactly that: it returns a context tied to
context.Background() plus a cancel function, and spins up a minimal
goroutine that cancels the context as soon as quit is closed. The
returned cancel must be called (deferred at the call site) so the
goroutine exits when the enclosing operation completes normally before
shutdown.

This is a pure utility with no policy and no default timeout, so
callers remain in full control of lifetime.
2026-04-10 19:16:49 -07:00
Olaoluwa Osuntokun
4e0992fa4e actor: add CompleteWith and AwaitFuture generic package-level helpers
In this commit, we add two package-level generic helpers to the actor
module as part of the broader chan error -> Future[error] migration in
the discovery package.

CompleteWith[T](p Promise[T], val T) is a one-liner convenience wrapper
over p.Complete(fn.Ok(val)). It lets callers complete a promise with a
plain value without constructing an fn.Result inline, which cuts noise
at every completion site.

AwaitFuture[T](ctx, f Future[T]) (T, error) provides the symmetric
receive side: it blocks until the future resolves or the context is
cancelled and returns the value and any context error unpacked from
the fn.Result, matching the (val, err) convention callers expect.

Both functions are deliberately thin (no policy, no timeout, no new
state) so they compose freely with higher-level helpers built on top,
e.g. discovery.AwaitGossipResult.

go.mod is updated to pin the actor module via a local replace directive
so the rest of the lnd module picks up these additions without waiting
for a tagged release.
2026-04-10 19:16:49 -07:00
ziggieXXX
0a2f48625b
Merge pull request #10684 from ziggie1984/postgres-network-separation
sqldb: add network-mismatch safeguard for native-SQL backends
2026-04-10 13:06:23 +02:00
ziggie
e744e19ba7
lnd: skip network validation when migrations are skipped
Skip the chain_params network check when startup is explicitly
configured to skip SQL migrations. In that mode the schema is assumed
to already be managed externally, and the chain_params table may not
exist yet. Avoid failing startup on a missing table in this path.
2026-04-10 12:04:25 +02:00
ziggie
a10cd1699f
docs: add release note for network separation safeguard
This is in particular important when running with a postgres
backend.

This only works if you run LND with the native sql flag but
people should run it with this flag from 21 on anyways.
2026-04-10 12:04:04 +02:00
ziggie
ccabac60df
itest: cover postgres network separation 2026-04-10 12:03:55 +02:00
ziggie
d988682e97
sqldb: add validate-network unit tests 2026-04-10 12:03:54 +02:00
ziggie
5eebbb0e69
lnd: validate native-sql network on startup 2026-04-10 08:13:06 +02:00
ziggie
3697f63ab1
sqldb: add new chainparam store 2026-04-10 08:13:06 +02:00
ziggie
0b82a89fda
sqldb/sqlc: add chain_param schema and queries 2026-04-10 08:13:04 +02:00
Yong
9f77b52d7f
Merge pull request #10703 from yyforyongyu/10697-review-fixes
Fix `sqldb/v2` regressions
2026-04-10 12:30:21 +08:00
Olaoluwa Osuntokun
80572fe39e
Merge pull request #10726 from ziggie1984/fix-migration-table
sqldb: fix migration config consistency coverage
2026-04-09 17:20:03 -07:00
ziggie
bd7e950c87
sqldb: harden migration config consistency tests
Strengthen migration consistency coverage by checking the reverse
mapping from embedded SQL files to migrationConfig entries, deriving
previous schema state from slice order instead of Version, rejecting
schema version regressions, and asserting migration names match the
embedded SQL file stems.

Also fix the graph v2 migration config name to match the embedded
migration filename.
2026-04-09 22:09:23 +02:00
Elle
27e50765b6
Merge pull request #10717 from ellemouton/g175-filter-known-chan-ids-v2
graph/db: make FilterKnownChanIDs version-aware
2026-04-09 17:05:51 +05:45
ziggie
8a9f774f2e
sqldb: register migration 14 in migration config
Add the 000014_payments_no_fail_reason_index migration to the
main migration configuration so the global migration list stays in
sync with the embedded SQL schema files.
2026-04-09 12:58:09 +02:00
ziggieXXX
6dd4094c24
Merge pull request #10719 from ziggie1984/hodlqueue-stop-order-fix
htlcswitch: fix hodlQueue deadlock by stopping htlcManager first
2026-04-09 12:23:24 +02:00
Yong
839d19c72c
Merge pull request #10517 from ajaysehwal/android-16kb-update
build: add Android 16KB page size flags to Makefile
2026-04-09 17:09:24 +08:00
ajaysehwal
bb1b56f3fe docs: add release notes for Android 16KB page size 2026-04-09 14:09:51 +05:30
ziggie
15135222ea
docs: add release-notes for 21 2026-04-09 10:20:29 +02:00
ziggie
f550ac1f7c
htlcswitch: fix hodlQueue deadlock by stopping htlcManager first
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.
2026-04-09 10:02:30 +02:00
ajaysehwal
38575679be build: add Android 16KB page size flags to Makefile 2026-04-09 10:36:03 +05:30
Elle Mouton
4d85877a05
docs: add release note for FilterKnownChanIDs versioning 2026-04-09 10:45:49 +05:45
Elle Mouton
ebb199d215
graph/db: fix FetchChannelEdgesByID zombie fallback versioning
The zombie fallback in SQLStore.FetchChannelEdgesByID unconditionally
constructed a models.NewV1Channel regardless of the requested gossip
version. Use the passed version to select the correct constructor so
that v2 zombie edges carry the right version.

A new testFetchZombieEdgeVersioning versioned test verifies that
zombie edges returned by FetchChannelEdgesByID have the correct
gossip version for both v1 and v2.
2026-04-09 10:45:49 +05:45
Elle Mouton
12f8e50958
graph/db: add gossip version parameter to FilterKnownChanIDs
Add an explicit lnwire.GossipVersion parameter to FilterKnownChanIDs
on the Store interface, SQLStore, KVStore, and ChannelGraph. Since
FilterKnownChanIDs is always called from a version-scoped ChanSeries
context, a single version parameter is cleaner than per-item version
reads. A convenience wrapper on VersionedGraph preserves the existing
ChanSeries call-site signature by threading c.v automatically.
2026-04-09 10:45:49 +05:45
Elle Mouton
bcadafa1ab
graph/db: parameterize forEachChanInSCIDList with gossip version
Replace the hardcoded GossipVersion1 in forEachChanInSCIDList with an
explicit version parameter so the helper can be used for v2 channel
lookups. The caller in FilterKnownChanIDs now passes the version
through.
2026-04-09 10:04:09 +05:45
Elle
7cdf9762f5
Merge pull request #10716 from ellemouton/g175-graph-db-v2-test-conversion
graph/db: convert v1-only tests to versioned v1+v2 tests
2026-04-09 10:03:16 +05:45
yyforyongyu
7074419bfe
sqldb/v2: align sqlite idle defaults
Keep SQLite's default idle connection limit aligned with the
open connection limit so the default pool matches v1 behavior.

This is a follow-up regression fix to the restored open-connection
default in e263ea145. After that change, SQLite again defaulted to
SetMaxOpenConns(2), but SetMaxIdleConns still fell back to 6. Go
silently caps idle connections at the open limit, so nothing
crashed, but the configured idle default became misleading and no
longer matched v1.

Use cfg.MaxConns() as the inherited idle default, keep the
explicit MaxIdleConnections override, and add unit coverage for
the default and override cases. The mismatch was easy to miss
because the code still compiled and basic tests did not assert the
effective idle pool sizing.
2026-04-09 08:32:06 +08:00
yyforyongyu
115daef42a
sqldb/v2: scope retry rollbacks
Scope the safety-net rollback to each retry attempt instead of
storing one deferred rollback per loop iteration.

This keeps cleanup local to the active attempt while
preserving the existing commit and rollback behavior.
2026-04-09 08:31:56 +08:00
yyforyongyu
70ab2fc52b
sqldb/v2: validate migration sets
Validate migration descriptor ordering before executing a
migration stream so inconsistent metadata fails fast.

In addition to checking contiguous descriptor versions and keeping
LatestMigrationVersion aligned with the last descriptor, reject a
non-zero LatestMigrationVersion when the descriptor list is empty.
Without that guard, a set such as {LatestMigrationVersion: 5}
passed validation silently.

The validation intentionally focuses on version consistency.
Descriptor names remain optional metadata for debugging, and the
checks still run only when migrations execute, which means
SkipMigrations continues to bypass validation by design. Add unit
coverage for the new empty-descriptor case.
2026-04-09 08:31:32 +08:00
yyforyongyu
275fe497fb
sqldb/v2: harden fixture names
Sanitize Docker container names more aggressively and add a
random suffix so concurrent test runs do not collide on the
same fixture name.

Normalize unsupported characters, trim leading and trailing
punctuation, and keep the fallback name for cases that sanitize
down to nothing. Add unit coverage for the sanitizer so the
allowed name surface stays explicit.
2026-04-09 08:31:06 +08:00
yyforyongyu
5c067e7673
sqldb/v2: drop dead retry helper
Remove the unused txExecutorOptions retry helper so the package
only keeps the backoff logic that is actually used.

This avoids carrying a second retry API with different
semantics from the live exponential backoff path.
2026-04-09 08:30:41 +08:00
yyforyongyu
8be8964632
sqldb/v2: add executor backend
Make TransactionExecutor satisfy the BatchedTx contract by
providing Backend() and asserting the interface conformance at
compile time.

This was a latent interface mismatch rather than an immediately
triggered package-wide compile failure. The executor was
instantiated directly, but sqldb/v2 did not yet assert or use it
as a BatchedTx, so the missing method stayed hidden until a caller
tried to rely on the advertised interface.

At the same time, move Backend() onto BatchedQuerier so the lower-
level contract explicitly requires backend identity. That lets the
executor delegate directly instead of probing an anonymous
interface at runtime, which would have weakened the contract and
fell back to BackendTypeUnknown instead of failing at compile
time.

Keep the focused runtime test and the compile-time assertion so
future interface drift is caught immediately.
2026-04-09 08:30:41 +08:00
yyforyongyu
76fc6863d4
sqldb/v2: use BaseDB skip flag
Route migration skipping through the BaseDB field that each
store already initializes.

This makes the embedded state meaningful and keeps the runtime
migration behavior consistent across both backends.
2026-04-09 08:30:41 +08:00
yyforyongyu
a0a52734d8
sqldb/v2: align test helper args
Use the same NewTestDBWithVersion argument order across the
SQLite and Postgres test helpers.

This was a latent build-tag API mismatch rather than a current
package-wide compile failure. Any shared helper that called
NewTestDBWithVersion(t, set, version) would compile under one
backend tag and fail under the other, but the mismatch stayed
hidden because nothing in sqldb/v2 called the helper yet.
2026-04-09 08:30:41 +08:00
yyforyongyu
d8734aaa78
sqldb/v2: fix sqlite migration errors
Wrap SQLite programmatic migration setup failures with the
SQLite error helper instead of the Postgres helper.

Before this change, a MakeProgrammaticMigrations failure on the
SQLite path returned an error that mentioned Postgres, which made
backend-specific setup failures needlessly confusing to debug.
The package still compiled and behaved normally unless that narrow
error path was exercised, which is why it slipped through.

Add a targeted unit test that forces the failing constructor path
and asserts the returned error is attributed to SQLite rather
than Postgres.
2026-04-09 08:30:40 +08:00
yyforyongyu
82e3ce2987
sqldb/v2: restore sqlite conn limit
Restore the low default SQLite connection limit used in v1 so
the v2 store does not default to a Postgres-sized pool.

This is a real v2 regression from v1. The v1 store defines
DefaultSqliteMaxConns = 2 and routes SetMaxOpenConns through
cfg.MaxConns(), while v2 had fallen back to the generic
defaultMaxConns = 25. That change did not break compilation, but
it quietly changed runtime pool sizing in a way that is hostile
to SQLite's single-writer concurrency model and can increase lock
contention.

Add a small config helper and unit test so callers can still
override the limit explicitly while the default remains safe for
SQLite. The regression was easy to miss because existing tests
did not assert the effective default pool sizing.
2026-04-09 08:30:33 +08:00
yyforyongyu
2be43f4108
sqldb/v2: enforce require ssl mode
Apply the RequireSSL config knob when opening Postgres stores
so it cannot be silently ignored.

Before this change, sqldb/v2 exposed PostgresConfig.RequireSSL
but still opened cfg.Dsn verbatim, which meant RequireSSL=true
was a no-op. A caller could set RequireSSL=true together with a
DSN such as sslmode=disable and still establish a non-TLS
connection.

This is a v2-only API contract bug, not a v1 regression in DSN
handling: v1 never offered a separate RequireSSL flag and always
left TLS policy entirely up to the DSN. The fix rewrites the DSN
to use sslmode=require when needed, while preserving stricter
modes such as verify-ca and verify-full.

The bug was easy to miss because no test asserted that the boolean
flag changed the effective DSN or overrode an insecure sslmode.
Keep the focused DSN rewrite test because it proves the contract
without needing a live Postgres instance.
2026-04-09 08:30:01 +08:00
yyforyongyu
ab7f36f21a
sqldb/v2: fix no_sqlite target builds
Restore the no_sqlite shim so unsupported SQLite targets still
build the module.

Update the stub to satisfy the current migration interface and
restore the missing Postgres-only SQL error helpers. These
failures were easy to miss because the default developer and CI
paths build native SQLite targets, while the broken code only
showed up on no-SQLite architectures and build tags.
2026-04-09 08:29:34 +08:00
yyforyongyu
cfb7ae355a
sqldb/v2: align test helper build tags
Align the backend-swapped test helper files with the platforms
where their backing SQLite and Postgres helper implementations
actually exist.

This keeps the exported helper surface internally consistent even
on targets that current CI likely does not exercise. Before this
change, the SQLite helper file was still selected on no-SQLite
targets, and the Postgres helper file was still selected on
openbsd and netbsd under test_db_postgres even though the Docker
fixture was compiled out there.
2026-04-09 08:29:34 +08:00
yyforyongyu
093c1c7921
sqldb/v2: fix postgres time rewrite
Keep the Postgres schema rewrite keyed on " TIMESTAMP" so
CURRENT_TIMESTAMP is not rewritten while schema files are
adapted.

Add a focused unit test to keep the replacement aligned with the
existing v1 behavior.
2026-04-09 08:29:34 +08:00
Yong
9bf4f502c8
Merge pull request #10721 from yyforyongyu/fix-paymentdb
paymentsdb: restore sql payment store parity with kv
2026-04-09 08:11:25 +08:00
Olaoluwa Osuntokun
cb0474885d
Merge pull request #10683 from pinheadmz/bury-taproot-deployment
chainreg: accommodate buried taproot deployment in Bitcoin Core v31
2026-04-08 13:33:16 -07:00
yyforyongyu
414fcc6244
sqldb/sqlc: simplify non-terminal payment query
Simplify FetchNonTerminalPayments by collapsing the selector down to
two branches: payments that are not failed and have no settled attempt,
and payments that still have unresolved attempts. This keeps the same
non-terminal semantics while making the query easier to reason about.

Also add a partial index on payments(id) where fail_reason IS NULL to
speed up the startup selector branch that scans payments without a
recorded failure reason.
2026-04-09 02:47:57 +08:00
Matthew Zipkin
39639056ad
docs: update release notes 2026-04-08 10:41:58 -04:00
Yong
97fd0aa91a
Merge pull request #10722 from ziggie1984/ci/pr-severity-use-github-token
ci: use GITHUB_TOKEN instead of PAT for PR severity workflow
2026-04-08 22:23:11 +08:00
ziggie
77c566f2fa
ci: use GITHUB_TOKEN instead of PAT for PR severity workflow
The PR severity classifier only needs to run `gh pr view`, `gh pr edit`
(labels), and `gh pr comment`. All three operations are fully covered by
the built-in GITHUB_TOKEN given the existing permissions block:

  permissions:
    contents: read
    pull-requests: write
    issues: write

The workflow uses `pull_request_target`, which runs in the base repo
context, so GITHUB_TOKEN has write access even for fork PRs.

Inspection of the claude-code-action@v1 source confirmed that the only
internal call that would require `contents: write` is branch deletion,
which is never exercised here because Claude's tools are locked down to
`gh pr view/edit/comment` via --allowedTools.

This removes the dependency on the PR_SEVERITY_BOT_TOKEN PAT secret.
2026-04-08 14:49:16 +02:00
yyforyongyu
412db8ae70
paymentsdb: log unexpected nil attempt hashes
RegisterAttempt falls back to the payment identifier when an attempt
hash is nil so legacy data can still round-trip safely. In live router
code, however, a nil attempt hash should never happen for newly
registered attempts.

Add an error log on the fallback path so an unexpected nil attempt hash
is surfaced immediately instead of silently persisting the fallback
value.
2026-04-08 19:21:53 +08:00
yyforyongyu
d9c7c81811
itest: wait for active channels before replaying payments 2026-04-07 20:29:54 +08:00
yyforyongyu
d2d0cfa0d3
lntest: retry mempool snapshots during RBF churn 2026-04-07 20:29:53 +08:00
Elle Mouton
a390ddd08e
graph/db: tighten TestFilterChannelRangeVersionGuard assertions
Use isSQLDB to explicitly assert the expected outcome per backend:
SQL should succeed with empty results, KV should return
ErrVersionNotSupportedForKVDB.
2026-04-07 14:20:48 +05:45
Elle Mouton
b73fa5f3e0
graph/db: convert TestDisconnectBlockAtHeight to versioned test
Rename to testDisconnectBlockAtHeight and add it to the versionedTests
table so it runs against both v1 and v2 backends.
2026-04-07 14:13:44 +05:45
Elle Mouton
0d82676d53
graph/db: convert TestGraphZombieIndex to versioned test
Rename to testGraphZombieIndex and add it to the versionedTests table
so it runs against both v1 and v2 backends. The assertNumZombies
helper is updated to accept a gossip version parameter.
2026-04-07 14:07:41 +05:45
Elle Mouton
a9c9e76560
graph/db: convert TestLightningNodeSigVerification to versioned test
Rename to testLightningNodeSigVerification and add it to the
versionedTests table so it runs against both v1 and v2. The signing
step is version-specific (ECDSA for v1, Schnorr for v2) while the
verification path is shared.
2026-04-07 14:03:58 +05:45
Elle Mouton
f7b689d576
graph/db: convert TestNodePruningUpdateIndexDeletion to versioned test
Rename to testNodePruningUpdateIndexDeletion and add it to the
versionedTests table so it runs against both v1 and v2 backends.

The NodeUpdateRange is now built per-version: time-based for v1
and block-height-based for v2.
2026-04-07 13:57:45 +05:45
yyforyongyu
95e447f23e
paymentsdb: remove dead inflight helper code
After removing the old FetchAllInflightAttempts query API, the helper
types that only existed to batch load that path are no longer used.

Use make lint as evidence. It reports the old inflight helper types as
unused once the query API is gone.

Remove the obsolete helper structs and batch-loading function from the
payment SQL store so the remaining code matches the new inflight
recovery path.
2026-04-07 16:09:51 +08:00
yyforyongyu
60d6b74730
sqldb/sqlc: remove old inflight query API
FetchInFlightPayments no longer relies on the old
FetchAllInflightAttempts query surface once the non-terminal payment
query is in place.

Use make lint and the inflight recovery tests as evidence. The code
still passes once the old query and its generated bindings are removed.

Remove FetchAllInflightAttempts from payments.sql, regenerate the sqlc
bindings, and drop the matching SQLQueries interface method.
2026-04-07 16:09:51 +08:00
yyforyongyu
96cbd4a7ef
paymentsdb: use non-terminal payment query for recovery
The new FetchNonTerminalPayments query is available, but
FetchInFlightPayments still uses the old unresolved-attempt scan until
this commit.

Use the inflight recovery regression tests as evidence. They now pass
on both KV and SQL once the payment store is wired up to use the new
query.

Fix this by switching FetchInFlightPayments to the non-terminal
payment query and batch loading only the related attempt and route
data for those payment IDs.
2026-04-07 16:09:51 +08:00
yyforyongyu
4c163acfa9
sqldb/sqlc: add non-terminal payment query
FetchInFlightPayments needs a dedicated SQL query that can return
non-terminal payments without relying on the unresolved-attempt scan.
The first version of that query fixed correctness, but the follow-up
selector measurements showed a UNION-based shape was materially
faster while returning the same payment set.

Use the inflight regression tests as evidence. The tests still fail on
SQL before the Go payment store is wired up, but this commit adds the
final SQL surface the later wiring commit depends on.

Add FetchNonTerminalPayments to the SQL query set, regenerate the sqlc
bindings, add the PaymentAndIntent adapters for the new row type, and
use the UNION-based candidate selection so the final query shape lands
in one commit.
2026-04-07 16:09:43 +08:00
yyforyongyu
5d9756f8c3
paymentsdb: add inflight recovery regression tests
SQL FetchInFlightPayments only returns payments with an unresolved
attempt row. KV returns every non-terminal payment, including
retryable payments with only failed attempts and payments that have
been initialized but have not registered any HTLCs yet.

Add TestFetchInFlightPaymentsIncludesRetryablePayments and
TestFetchInFlightPaymentsIncludesInitiatedPayments as evidence. Both
tests pass on KV and fail on SQL before the fix.
2026-04-07 16:09:29 +08:00
yyforyongyu
53eea4d021
paymentsdb: preserve AMP attempt hashes in sql store
Live SQL writes stored the payment identifier in payment_hash for
each attempt. That works for legacy payments, but it breaks AMP
because the payment identifier is the SetID while each shard carries
its own HTLC hash.

Use TestRegisterAttemptPreservesAttemptHash as evidence. The test now
passes on both KV and SQL.

Fix this by persisting attempt.Hash when it is present and only
falling back to the payment identifier when the attempt hash is nil.
That restores KV parity for AMP attempt reloads.
2026-04-07 16:09:29 +08:00
yyforyongyu
e8c58fc0c8
paymentsdb: add AMP hash regression test
SQL writes store the payment identifier in payment_hash for each
attempt. That is wrong for AMP payments, because the payment
identifier is the SetID while each shard carries its own HTLC hash.

Add TestRegisterAttemptPreservesAttemptHash as evidence. It passes on
KV and fails on SQL before the fix because SQL reads the attempt hash
back as the payment identifier.
2026-04-07 16:09:29 +08:00
Matthew Zipkin
f831476127
chainreg: accommodate buried taproot deployment in Bitcoin Core v31
Removes "taproot" from deployments:
https://github.com/bitcoin/bitcoin/pull/26201

Adds "script_flags" field to RPC getdeploymentinfo:
https://github.com/bitcoin/bitcoin/pull/32998
2026-04-06 10:09:17 -04:00
ziggieXXX
167f03bb32
Merge pull request #10700 from ziggie1984/invoices-cursor-pagination
invoices+sqldb/sqlc: replace offset-based pagination with cursor-based
2026-04-01 20:31:04 +02:00
Yong
37ae765136
Merge pull request #10697 from ziggie1984/sqldb-v2-rebase
sqldb/v2: introduce sqldb/v2 module
2026-04-01 22:13:00 +08:00
ziggie
85c8e95514
docs: add release note for cursor-based invoice pagination 2026-04-01 10:55:43 +02:00
ziggie
e02f77ec9f
invoices+sqldb/sqlc: replace offset-based pagination with cursor-based
The invoice filter queries (FetchPendingInvoices,
FilterInvoicesBySettleIndex, FilterInvoicesByAddIndex,
FilterInvoicesForward, FilterInvoicesReverse) all used LIMIT+OFFSET for
internal pagination. This causes SQLite to build an ephemeral temp
B-tree for every page to implement the OFFSET skip, making each
successive page O(offset+limit). On nodes with large invoice histories
this compounds into a significant CPU cost — profiling showed
FilterInvoicesReverse consuming 53% of total CPU, with _sqlite3BtreeInsert
and _balance_nonroot (2.4s combined) appearing inside the SELECT due to
the temp B-tree being built and rebalanced to skip rows.

Replace the OFFSET loop (queryWithLimit) with cursor-based pagination
across all four callers in sql_store.go:

- FetchPendingInvoices: add id_cursor param, advance cursor to
  last_id + 1 each page.
- InvoicesSettledSince: add id_cursor param alongside the existing
  settle_index lower bound, advance cursor to last_id + 1 each page.
- InvoicesAddedSince: cursor starts at idx+1, advances to last_id+1.
- QueryInvoices: forward cursor starts at IndexOffset+1 and advances
  by +1; reverse cursor starts at IndexOffset-1 (or MaxInt64) and
  advances by -1. Inclusive SQL bounds (>= / <=) are preserved so
  query semantics and all existing callers are unchanged.

The queryWithLimit helper is removed as it has no remaining callers.

Each page now performs a single PK seek + forward scan of exactly
page_size rows with no temp sort structure, matching the cursor-based
pattern already used by the payments filter queries.
2026-04-01 10:55:42 +02:00
Olaoluwa Osuntokun
0a87772b3f
Merge pull request #10695 from ellemouton/fix-rbf-closer-test-flakes
lnwallet/chancloser: fix RBF closer test flakes
2026-03-31 16:19:14 -07:00
Olaoluwa Osuntokun
af389869e2
Merge pull request #10693 from erickcestari/validate-musig2-nonces-in-local-nonces
lnwire: validate MuSig2 nonces in LocalNoncesData decode
2026-03-31 16:15:36 -07:00
Viktor Torstensson
1b3bea561e
sqldb/v2: limit MigrationExecutor interface
This commit limits the MigrationExecutor interface due to the following
reasoning:

1. SkipMigrations() and DefaultTarget() should not be on the interface
Both are only used by ApplyAllMigrations, which immediately passes the
results back into the same executor. They are internal implementation
details and should be folded into ExecuteMigrations itself.

2. SetSchemaVersion and GetSchemaVersion are test-only but on the
production interface Every caller of these in sqldb/v2 is in test files.
The SetSchemaVersion comment even says "USE WITH CAUTION" — dangerous
test utilities should not be on an interface that every real consumer
must implement. They should be accessible on the concrete types only and
used directly in tests without going through the interface.

3. ExecuteMigrations should not take a MigrationTarget parameter for the
normal path On the normal startup path, callers just do
executor.ExecuteMigrations(executor.DefaultTarget(), stream) — asking
the executor for its default and handing it straight back. The method
should run to latest by default; a version override for tests can live
on the concrete type instead.
2026-03-31 20:11:36 +02:00
Viktor Torstensson
9946d521eb
sqldb/v2: rename MigrationConfig to MigrationDescriptor 2026-03-31 20:11:35 +02:00
Viktor Torstensson
cff8836e33
sqldb/v2: add MaxIdleConnections & ConnMaxLifetime sqlite opts 2026-03-31 20:11:35 +02:00
Viktor Torstensson
1819b9867e
sqldb/v2: clarify config options docs
The documentation for the `SqliteConfig.MaxConnections`,
`PostgresConfig.MaxOpenConnections` and
`PostgresConfig.MaxIdleConnections` previously stated that an unlimited
number was used when the value was set to 0. This is not the case
however, as setting the value to 0 will result in the default values
being used.
2026-03-31 20:11:35 +02:00
Viktor Torstensson
58f757a8c7
sqldb/v2: ensure SqliteConfig.MaxConnections is used 2026-03-31 20:11:35 +02:00
Viktor Torstensson
0c8f52115d
sqldb/v2: use defaultMaxIdleConns in SqliteStore
The code previously used `defaultMaxOpenConns` for both the maximum
number of open connections and the maximum number of idle connections in
the `SqliteStore`.

This commit updates the code to use `defaultMaxIdleConns` for the
maximum number of idle connections.
2026-03-31 20:11:35 +02:00
Viktor Torstensson
4219e5aebd
sqldb/v2: rename the postgresErrMsgs list
Rename the `postgresErrMsgs` list to `postgresRetriableErrMsgs`, in
order to clarify its usage.
2026-03-31 20:11:35 +02:00
Viktor Torstensson
a726dbd075
sqldb/v2: rename ErrRetriesExceeded error
rename the `ErrRetriesExceeded` error to `ErrTxRetriesExceeded`.
2026-03-31 20:11:35 +02:00
Viktor Torstensson
422dd5d231
sqldb/v2: move Sqlite test helpers to separate file 2026-03-31 20:11:34 +02:00
Viktor Torstensson
12d97cc4ea
sqldb/v2: set predictable NewTestPgFixture container name 2026-03-31 20:11:34 +02:00
Viktor Torstensson
07fdd0f4c4
sqldb/v2: clarify no_sqlite SqliteStore intent
The docs of the `SqliteStore` for no sqlite build environments
previously didn't clarify that the actual `SqliteStore` implementation
under such build tag environments, didn't actually implement a real
sqlite store. This commit clarifies that in the docs.
2026-03-31 20:11:34 +02:00
Viktor Torstensson
afc1a675d1
sqldb/v2: rename test db helper files
Previously, the test db helper files were suffixed with "_test", which
would indicate that the files specifically contained tests.
However, these files actually contain helper functions to be used
in tests, and are not tests themselves. To better reflect their
purpose, the files have been renamed to instead be prefixed with
"test_".
2026-03-31 20:11:34 +02:00
Viktor Torstensson
13d0cf1111
sqldb/v2: sync features with tapd's sqldb package
In order to make it possible to replace `tapd`'s internal `sqldb`
package with the new generic `sqldb/v2` package, we need to make sure
that all features and functionality that currently exist in the `tapd`
package are also present in the new `sqldb/v2` package.

This commit adds such additional missing features to the `sqldb/v2`
package.
2026-03-31 20:11:34 +02:00
Viktor Torstensson
472deadc18
sqldb/v2: introduce sqldb/v2 BaseDB
This commit updates the definition of the `BaseDB` struct to decouple
it from lnd`s `sqlc` package. We also introduce new fields to the struct
to make it possible to track the database type used at runtime.
2026-03-31 20:11:34 +02:00
Viktor Torstensson
b0c7c570da
sqldb/v2: Use MigrationStream for migrations
This commit updates the `sqldb/v2` package to utilize the new
`MigrationStream` type for executing migrations, instead of passing
`[]MigrationConfig`'s directly.
2026-03-31 20:11:33 +02:00
Viktor Torstensson
1c833c5c90
sqldb/v2: introduce MigrationStream
This commit introduces a new struct named `MigrationStream`, which
defines a structure for migrations SQL migrations.

The `MigrationStream` struct contains the SQL migrations which will be
applied, as well as corresponding post-migration code migrations which
will be executed afterwards. The struct also contains fields which
define how the execution of the migrations are tracked.

Importantly, it is also possible to define multiple different
`MigrationStream`s which are executed, to for example define one `prod`
and one `dev` migration stream.
2026-03-31 20:11:33 +02:00
Viktor Torstensson
d7a5127cc0
sqldb/v2: move all non lnd-specific v1 code to v2
This commit moves all non lnd-specific code of sqldb/v1 to the new
sqldb/v2 module.

Note however, that without additional changes, this package still needs
to reference lnd, as references to the lnd `sqlc` package is required
without further changes. Those changes will be introduced in the
upcoming commits, to fully decouple the new sqldb/v2 module from lnd.
2026-03-31 20:11:33 +02:00
Viktor Torstensson
aa1725a875
sqldb/v2: add base for sqldb/v2 module
In the upcoming commits, we will introduce a new sqldb module, sqldb
version 2.

The intention of the new sqldb module, is to make it generalizable so
that it contains no `lnd` specific code, to ensure that it can be reused
in other projects.

This commit adds the base of the new module, but does not include any
implementation yet, as that will be done in the upcoming commits.
2026-03-31 20:11:32 +02:00
Yong
db41d4f81f
Merge pull request #10692 from ellemouton/graph-node-horizon-speedup
sqldb: optimize v1 node horizon query and indexes
2026-03-31 22:12:50 +08:00
Erick Cestari
05f04f748f
lnwire: validate MuSig2 nonces in LocalNoncesData decode
Validate each nonce in decodeLocalNoncesData with
ValidateMusig2Nonce to reject malformed entries.
2026-03-31 09:22:00 -03:00
Elle Mouton
7e549631c5
lnwallet/chancloser: use wait.DefaultTimeout for state transitions
The hardcoded defaultTimeout (previously 500ms, then 5s) used in
assertStateTransitions was too tight for CI runners, especially
under coverage instrumentation or remote DB backends (postgres).

Use wait.DefaultTimeout which automatically adapts to the build
environment: 30s on standard platforms, 60s+ for remote DB builds,
and 60s on Windows.
2026-03-31 12:26:38 +02:00
Elle Mouton
1f8b558c0e
lnwallet/chancloser: remove racy "no more states" check
The assertStateTransitions helper contained a non-blocking select
after consuming expected states to verify no additional transitions
occurred. This check is inherently racy: the state machine goroutine
can emit the next transition before the non-blocking select runs,
causing spurious "unexpected state transition" failures.

This was the primary cause of TestRbfCloseErr flakes (~29% failure
rate under coverage instrumentation).

To preserve strictness, add a post-Stop() quiet-period check in
stopAndAssert(). After the state machine is stopped, no further
transitions should be produced, so draining the subscriber channel
there is deterministic and catches any unexpected stragglers.

Also fix the CloseErr restart test paths (send_offer_restart,
recv_offer_restart) which were under-consuming transitions. Both
ClosePending and CloseErr produce an extra ClosingNegotiation
emission on restart via internal requeue, so the helpers now take
an explicit expectExtraTransition flag rather than the ambiguous
iteration bool.

Finally, register the state subscriber before Start() to avoid
racing with the initial state notification emitted by driveMachine.
The old ordering (Start then RegisterStateEvents) could miss the
first transition entirely on slow CI runners, causing a permanent
timeout in assertStartupAssertions.
2026-03-31 12:26:38 +02:00
Elle Mouton
f3d9b1f2a4
docs: add release note for v1 node horizon query optimization 2026-03-31 12:05:45 +02:00
Elle Mouton
2fda82b68c
graph/db: add node horizon index benchmark
Add BenchmarkNodeHorizonIndex to compare query performance under old
vs new index configurations for NodeUpdatesInHorizon. Tests both
all-nodes and public-only variants against native SQLite and
optionally Postgres backends, swapping indexes via DDL between runs.
2026-03-31 12:05:36 +02:00
Elle Mouton
561edf8c96
sqldb/sqlc: split public-only node horizon query and upgrade channel indexes
Split GetNodesByLastUpdateRange into two query variants: one for all
nodes and a new GetPublicNodesByLastUpdateRange for public-only nodes.
The public-only variant uses two separate EXISTS checks (one per
node_id column) instead of a single OR, allowing the planner to do
direct index probes on each channel node-id index.

Also upgrade the channel node-id indexes from single-column
(node_id_1) and (node_id_2) to composite (node_id_1, version) and
(node_id_2, version) to support version-aware public node checks
while preserving usefulness for node-centric lookups.
2026-03-31 12:04:24 +02:00
Elle Mouton
2cf8b7bd04
sqldb/sqlc: add version filter and composite index for v1 node horizon query
The v1 GetNodesByLastUpdateRange query was missing an explicit
`WHERE version = 1` filter and used a single-column index on
`graph_nodes(last_update)` which didn't match the full query ordering
shape `ORDER BY last_update, pub_key`, requiring an extra sort step.

Add `WHERE version = 1` to the query for correctness and replace the
index with a composite `(version, last_update, pub_key)` index that
covers the filter, range scan, and pagination ordering together.

Note: the migration 000009_graph_v2 file is edited directly (dropping
the old index and creating the new one) rather than adding a new
migration, since this migration has not been included in a release yet.
2026-03-31 11:58:58 +02:00
Yong
324e10f58d
Merge pull request #10691 from ellemouton/g175-horizon
graph/db: version horizon queries for v1/v2 gossip
2026-03-31 17:49:50 +08:00
Elle Mouton
f38daf81dd
docs: add release note for versioned graph horizon queries 2026-03-31 09:14:50 +02:00
Elle Mouton
7f5be5a494
graph/db: add v2 block-height path for ChanUpdatesInHorizon
Add GetChannelsByPolicyBlockRange SQL query and wire it into
SQLStore.chanUpdatesInHorizonV2. This mirrors the existing v1
time-based query but filters on policy block_height instead of
last_update, using the same [start, end) exclusive-end semantics
and (max_block_height, channel_id) compound cursor pagination.

Also adds extractMaxBlockHeight helper (returns the max of both
policies' block heights for cursor tracking) and
buildChannelFromBlockRangeRow (structurally identical to the v1
variant but accepts the distinct sqlc-generated row type). The
extractChannelPolicies type-switch is extended with a case for the
new GetChannelsByPolicyBlockRangeRow type.
2026-03-31 09:14:49 +02:00
Elle Mouton
c14a79c0ae
graph/db: add v2 block-height path for NodeUpdatesInHorizon
Add GetNodesByBlockHeightRange SQL query and wire it into
SQLStore.nodeUpdatesInHorizonV2. This mirrors the existing v1
time-based query but filters on (version, block_height) instead of
last_update, using the same [start, end) exclusive-end semantics
and (block_height, pub_key) compound cursor pagination.

The public-node filter for v2 checks for channels with a non-empty
channel announcement signature (c.signature), matching the v2
protocol's public channel indicator.
2026-03-31 09:14:49 +02:00
Elle Mouton
ccbe7d696b
sqldb: add composite indexes for v2 block-height horizon queries
Add composite indexes on graph_nodes and graph_channel_policies for
the upcoming v2 block-height-based horizon queries.

The v2 gossip protocol uses block heights instead of unix timestamps
for ordering node announcements and channel updates. The v2
NodeUpdatesInHorizon and ChanUpdatesInHorizon query paths will
filter on WHERE version = @v AND block_height >= start AND
block_height < end. Without these indexes, those queries would
require full table scans.

For nodes, the index is (version, block_height, pub_key). Including
pub_key covers the ORDER BY (block_height, pub_key) clause and
allows direct cursor seeks for pagination, avoiding an extra sort.

For channel policies, the index is (version, block_height). The
pagination cursor uses a CASE expression across two joined policy
rows so the index cannot cover the ORDER BY — the two leading
columns are sufficient for the range scan.
2026-03-31 09:14:49 +02:00
Elle Mouton
cdd1979242
discovery: replace chain param with ctx in UpdatesInHorizon
Replace the unused chainhash.Hash parameter in
ChannelGraphTimeSeries.UpdatesInHorizon with context.Context. The
chain parameter was never consulted by the implementation since the
graph is not chain-scoped. The context is threaded through to the
underlying graph DB queries that need it.
2026-03-31 09:14:49 +02:00
Elle Mouton
fc7204ca25
graph/db: remove ChannelGraph horizon wrappers, use VersionedGraph
The ChannelGraph.NodeUpdatesInHorizon and
ChannelGraph.ChanUpdatesInHorizon methods were only used in tests.
All production callers already use VersionedGraph (which supplies
the gossip version from its embedded field).

Remove the ChannelGraph wrappers and update tests to instantiate a
VersionedGraph via NewVersionedGraph(MakeTestGraph(t), v1) instead,
dropping the explicit version parameter from horizon calls.
2026-03-31 09:14:49 +02:00
Elle Mouton
497d479440
graph/db: version NodeUpdatesInHorizon and ChanUpdatesInHorizon
Replace the (startTime, endTime time.Time) parameters on
NodeUpdatesInHorizon and ChanUpdatesInHorizon with
(v GossipVersion, r NodeUpdateRange/ChanUpdateRange). The range
types enforce version-correct bounds at the type level: v1 uses unix
timestamps, v2 will use block heights.

The KV store rejects non-v1 versions since it only stores v1 data.
The SQL store dispatches to version-specific helpers
(nodeUpdatesInHorizonV1, chanUpdatesInHorizonV1); the v2
block-height paths return an error for now and will be wired up in
follow-up commits.

VersionedGraph wrappers supply the version from the embedded field,
so callers only pass the range.
2026-03-31 09:14:49 +02:00
Elle Mouton
8b5d21a8f7
graph/db: add ChanUpdateRange and NodeUpdateRange types
Add version-aware range types for channel and node update horizon
queries. V1 gossip uses unix timestamps for ordering while v2 uses
block heights, so each range type validates that the correct bound
type is provided for the requested gossip version.

These types will be used in follow-up commits to version the
NodeUpdatesInHorizon and ChanUpdatesInHorizon Store methods.
2026-03-31 09:14:49 +02:00
Elle Mouton
57a32b9232
graph/db: use exclusive end time for horizon queries per BOLT 07
BOLT 07 specifies that gossip_timestamp_filter range semantics are
"greater or equal to first_timestamp, and less than first_timestamp
plus timestamp_range", i.e. [start, end). Three of the four
implementations (KV ChanUpdatesInHorizon, KV NodeUpdatesInHorizon,
SQL NodeUpdatesInHorizon) were incorrectly using an inclusive end
time (<= instead of <). Only SQL ChanUpdatesInHorizon was correct.

This commit fixes the KV store's fetchNextChanUpdateBatch and
fetchNextNodeBatch to use >= (instead of >) for the end time break
condition, and < (instead of <=) for the hasMore check. It also
fixes the SQL GetNodesByLastUpdateRange query to use < instead of <=
on the end_time bound.

All godocs are updated to reference the BOLT 07 spec language and
explicitly document the [start, end) range semantics. New dedicated
tests (TestNodeUpdatesInHorizonExclusiveEnd and
TestChanUpdatesInHorizonExclusiveEnd) verify that items at exactly
the end time are excluded while items at the start time are included.
2026-03-31 09:14:48 +02:00
ziggieXXX
137e5b1a16
Merge pull request #10666 from ziggie1984/fwd-history-purge
multi: add DeleteForwardingHistory to selectively purge old forwarding history
2026-03-30 18:45:03 +02:00
ziggieXXX
d91e19c4a8
Merge pull request #10219 from gijswijs/onion-messaging-2
Async handling of Onion Messages with `BackpressureQueue`
2026-03-30 18:00:39 +02:00
ziggie
a5fcc8191a
docs: add release-notes for LND 21 2026-03-30 17:39:46 +02:00
ziggie
c14dcf0ef8
docs: add forwarding history privacy guide
In this commit, we add a guide explaining the privacy implications of
retaining forwarding history and how to use DeleteForwardingHistory to
implement a data retention policy. The guide covers the CLI interface,
batch size tuning, cron-based automation, database compaction, fee
accounting considerations, and privacy best practices.
2026-03-30 17:39:46 +02:00
ziggie
45fe63ca8c
itest: add integration tests for DeleteForwardingHistory
In this commit, we add integration tests for the DeleteForwardingHistory
RPC covering four scenarios: basic deletion of all events, partial
deletion by time range, empty database handling, and idempotency.
A time format test validates both the relative duration and absolute
timestamp code paths end-to-end.

Bob's node is started with --routerrpc.min-fwd-history-age=2s so the
tests can exercise the minimum age guard without waiting an hour.
2026-03-30 17:39:46 +02:00
ziggie
7d90b8e755
cmd: add deletefwdhistory CLI command
In this commit, we add the lncli deletefwdhistory command that wraps
the DeleteForwardingHistory RPC. The command accepts a time
specification in one of two forms:

  --age=<duration>   relative duration, e.g. "-90d", "-1M", "-720h"
  --before=<unix>    absolute Unix timestamp in seconds

An interactive confirmation prompt is shown before deletion proceeds,
which can be suppressed with --force/-f for unattended automation.
The --batch_size flag controls events deleted per database transaction
(default 10000, max 50000).

The response is printed as JSON, consistent with other lncli commands.
2026-03-30 17:39:46 +02:00
ziggie
6dd3e4864a
lntest: add DeleteForwardingHistory to RPC harness
In this commit, we extend the test harness RPC wrapper to expose the
new DeleteForwardingHistory method, following the established pattern
for router RPC calls with automatic error handling and logging.
2026-03-30 17:39:46 +02:00
ziggie
b3b05ddaf3
rpcserver: wire ForwardingLog into router backend
In this commit, we pass the node's ForwardingLog into the RouterBackend
alongside the MinFwdHistoryAge configuration value, completing the
dependency injection chain from the RPC handler down to the database
layer.
2026-03-30 17:39:46 +02:00
Gijs van Dam
e87f4bfb6f onionmessage: use BackpressureMailbox for onion peer actors
This commit adds per-peer backpressure control to the onion message
actor system by introducing a BackpressureMailbox that uses Random
Early Detection (RED) to probabilistically drop messages when the
per-peer queue depth exceeds a configurable threshold.

The OnionActorFactory type now accepts variadic ActorOptions, allowing
the spawn call site (brontide) to provide per-peer mailbox configuration.
A DefaultOnionActorOpts helper provides the standard RED thresholds so
callers don't need to wire up the BackpressureMailbox manually.

Key changes:
- OnionActorFactory signature extended with ...ActorOption[*Request,
  *Response] so backpressure policy is no longer baked into the factory.
- NewOnionActorFactory drops its shouldDrop parameter; it forwards opts
  through to serviceKey.Spawn.
- DefaultOnionActorOpts(shouldDrop) returns the default backpressure
  options (BackpressureMailbox + DefaultOnionMailboxSize).
- peer.Config gains OnionActorOpts callback for per-peer customization.
- server.go creates default opts once and returns them for every peer.
2026-03-28 12:42:53 +01:00
Gijs van Dam
dd917dc97c actor: add BackpressureMailbox and custom mailbox support
Add BackpressureMailbox, a Mailbox implementation backed by
queue.BackpressureQueue that consults a queue.DropCheckFunc on every
Send/TrySend to enable RED-style load shedding before the mailbox is
full.

Add MailboxFactory type and ActorOption functional options
(WithMailboxFactory, WithMailboxSize) so callers can inject custom
mailbox implementations when spawning actors via RegisterWithSystem
or ServiceKey.Spawn.
2026-03-28 12:42:53 +01:00
Gijs van Dam
ea8a665772 queue: add BackpressureQueue[T] with Random Early Drop
Add a generic BackpressureQueue that uses a DropPredicate to proactively
shed load before the queue is completely full.

Two predicate types are provided:
- DropCheckFunc: length-only drop decision (func(queueLen int) bool)
- DropPredicate[T]: item-aware drop decision

RandomEarlyDrop returns a DropCheckFunc since RED only considers queue
depth. The AsDropPredicate helper adapts it to DropPredicate[T] for use
with BackpressureQueue.

In addition to the blocking Enqueue/Dequeue methods, the queue exposes
TryEnqueue (non-blocking send with drop check), Len, ReceiveChan, and
Close. These are needed by the actor package's BackpressureMailbox which
uses BackpressureQueue as its core buffer while implementing the Mailbox
interface's select-based iteration and lifecycle methods.

Property-based tests using pgregory.net/rapid verify queue invariants
(capacity bounds, FIFO ordering, model consistency) across randomized
enqueue/dequeue sequences with RED enabled.
2026-03-28 12:42:53 +01:00
Olaoluwa Osuntokun
f297c4782e
Merge pull request #10063 from lightningnetwork/taproot-rbf
multi: add taproot support to the new RBF close flow
2026-03-27 16:17:44 -07:00
Olaoluwa Osuntokun
fa9163acd4 lnwallet/chancloser: fix flaky test assertions
Restore the defaultTimeout constant (500ms) that was lost during the
PR's commit squash, where the hardcoded 10ms replaced it. The 10ms
value was too tight and caused timeouts under -race -count=N.

Also fix assertSingleRemoteRbfIteration to consume both iteration
transitions in a single assertStateTransitions call. When
iteration=true, the state machine emits two ClosingNegotiation
transitions from a single event (via internal events). Making two
separate assertStateTransitions calls races with the "no more states"
check, which could drain the second transition before the second
assertion consumed it.
2026-03-27 16:16:51 -07:00
Olaoluwa Osuntokun
d7302cfc58 lnwallet/chancloser: move nonce rotation to LocalOfferSent
Move the RemoteCloseeNonce update from updateAndValidateCloseTerms to
LocalOfferSent.ProcessEvent. This keeps updateAndValidateCloseTerms
focused on close term validation, and makes the nonce rotation point
explicit in the state machine — it happens when processing the
LocalSigReceived event, alongside signature extraction.

Update TestNextCloseeNonceStorageFromClosingSig to verify that
updateAndValidateCloseTerms no longer modifies RemoteCloseeNonce.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
ed1e5411d7 lnwallet/chancloser: remove dead code and wrap errors
Remove the redundant `remoteMusig \!= nil` check inside the
`IsTaproot()` guard in sendShutdownEvents. Since IsTaproot() requires
both LocalMusigSession AND RemoteMusigSession to be non-nil, the nested
nil check can never be false.

Also wrap bare `return nil, err` with context in LocalOfferSent and
RemoteCloseStart ProcessEvent methods for prepareClosingSignatures,
CompleteCooperativeClose, and createLocalCloseeSignature calls.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
d9284abebc lnwallet/chancloser: address lint and PR review feedback
Fix all lint issues across the taproot RBF coop close changes:

- Fix line length violations (ll) by wrapping long lines and adding
  nolint:ll where wrapping would hurt readability.
- Fix nlreturn: add blank lines before return statements.
- Fix misspell: correct "siganture" typos.
- Fix forcetypeassert: add checked type assertions.
- Fix nonamedreturns: remove named returns from function signatures.
- Fix usetesting: replace context.Background() with t.Context().
- Fix unused: remove unused remoteSchnorrSig variable.
- Fix whitespace: add newlines after multi-line func signatures.
- Fix gocritic appendAssign warning.
- Fix gci: correct import ordering.

Also address PR review comments from @erickcestari:

- Remove nonce cache in ClosingNonce() to prevent future footguns.
- Rename extractSigAndNonce to extractSigAndNonceFromClosingSig.
- Rename extractSigAndNonceFromComplete to
  extractSigAndNonceFromClosingComplete with channel type validation.
- Replace env.RemoteMusigSession \!= nil with env.IsTaproot().
- Swap manual mocks to mock.Mock in musig_nonce_order_test.go.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
55854e1dcd docs: add taproot RBF coop close to 0.21.0 release notes 2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
4f669cefa8 lnwallet/chancloser: split taproot/non-taproot test helpers
Split testSendOfferRbfIterationLoop, testRecvOfferRbfLoopIterations,
and testSendOfferIterationNoDust into separate taproot and non-taproot
variants. This removes the isTaproot bool parameter and inlines each
branch, making the tests easier to read and maintain.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
f4fff1726b lnwallet/chancloser: fix MuSig2 nonce reuse across RBF rounds
MusigChanCloser.ClosingNonce() cached the local nonce and returned
the same one on subsequent calls. Since each RBF round creates a new
MuSig2 session via ProposalClosingOpts() but passes the same SecNonce,
the btcd library's per-session nonce reuse guard was bypassed (fresh
Session each round). Signing different closing transactions (different
fees/sighashes) with the same secret nonce enables private key
extraction via simple linear algebra on the partial signatures.

Fix by adding ClearNonce() to the MusigSession interface and calling
it after each signing round completes, forcing fresh nonce generation
on every RBF iteration. Also fix a rebase issue where
updateAndValidateCloseTerms was not extracting NextCloseeNonce from
ClosingSig messages for subsequent RBF rounds.

lnwallet/chancloser: fix MuSig2 nonce reuse across RBF rounds

MusigChanCloser.ClosingNonce() cached the secret nonce and returned
the same one across RBF rounds. Since each round creates a new
MuSig2 session via ProposalClosingOpts() but passes the same
SecNonce, signing different closing transactions with different
sighashes enables private key extraction.

Fix this by:

1. Storing the full MusigPartialSig from LocalCloseStart in the
   LocalOfferSent state, eliminating the second CreateCloseProposal
   call in prepareClosingSignatures. This was also flagged in PR
   review as wasteful.

2. Adding InvalidateNonce() to the MusigSession interface. After
   the closer round completes (CompleteCooperativeClose), the closer
   nonce is invalidated so the next RBF round generates fresh. For
   the closee, the nonce is invalidated before generating the next
   closee nonce in createClosingSigMessage.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
e2a91b235f lnwallet/chancloser: fix comments and wrap errors for debugging
Fix incorrect comment about PartialSigWithNonce nonce - it's the JIT
closer nonce for current session verification, not a closee nonce for
future RBF. Wrap errors from validateSigFields and CreateCloseProposal
with additional context to aid debugging.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
a0cf146627 lnwallet/chancloser: use AND for IsTaproot check, use partialSigToWireSig
Change IsTaproot() from OR to AND: both LocalMusigSession and
RemoteMusigSession must be set to consider the channel taproot. This
prevents panics from nil session access when only one session is
initialized.

Also use partialSigToWireSig helper for consistency in
extractSigAndNonceFromComplete instead of inline conversion.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
d4da350251 lnwire: reject messages with both regular and taproot signatures
Add decode-time validation to closing_complete and closing_sig that
rejects messages containing both regular ECDSA and taproot partial
signatures. This provides defense-in-depth rather than relying solely
on state machine validation.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
1a50eee1c2 lnwallet/chancloser: address PR review comments
Fix several issues raised in PR review:
- Use safe type assertion in createClosingSigMessage to avoid panic if
  localSig is not *MusigPartialSig.
- Fix typo "taprotot" -> "taproot" in comment.
- Remove unnecessary type argument in NewTaprootSigType.
- Simplify nested if to single condition for taproot nonce generation.
- Fix typo "once the no updates" -> "once there are no updates" in
  test comments.
- Move misplaced TestRbfCloseClosingNegotiationLocal doc comment to
  the correct function definition.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
763fea9209 lnwallet/chancloser: fix rbf close docs 2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
ca60f9c32e lnwallet/chancloser: fix local session nonce rotation bug
We weren't properly using the latest closee nonce from the remote party
when signing.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
7197fd219f chancloser: update RBF close to match current flow 2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
34a86ca800 multi: fix nonce handling bug
In this commit, we fix a nonce handling bug. The bug was unnoticed until
interop testing due to some inadvertent mutation. Before this commit, in
peer/brontide.go, we used the _same_ instance of the musig2 chan closer,
which masked the bug.

The issue was that we would attempt to generate a siganture for the
remote party _before_ we had applied their JIT nonce to our remote (used
to sign their close txn) musig session.

We first created a new test to confirm the issue (in peer, as it needed
to be in order to avoid a circular dep test). Without these changes, the
test fails.

The fix is two fold:
 1. Create two independent musig2 chan closers.
 2. Update the ordering to apply their nonce before we generate a
signature.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
1d2fa8dccb lnwallet/chancloser: revamp sig type parsing to be spec compliant
In this commit, we revise the sig type parsing to make the control flow
clearer, and also to be spec compliant. Before we would error out if
_both_ the CloserNoClosee and the CloserAndClosee fields were set.

lnwallet/chancloser: fix priority ordering for rbf sig parsing

We need to parse the sigs in a strict order, as it's possible for a
party to send more than one siganture.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
57def3bc00 chancloser: update RBF close documentation for taproot support
In this commit we, update the RBF cooperative close documentation to
comprehensively cover the taproot channel closing flow. The documentation
now explains the JIT nonce pattern, asymmetric signature roles, and the
complete nonce exchange protocol for taproot channels.

Key additions include detailed explanations of how nonces flow through
the RBF process, the distinction between closer and closee roles, and
the specific wire message extensions for PartialSigWithNonce and
NextCloseeNonce fields. The documentation also covers validation
requirements and implementation notes specific to taproot channels.

This documentation provides a complete reference for understanding
and implementing the enhanced taproot RBF cooperative close protocol.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
85adad753b multi: wire taproot RBF support throughout the stack
In this commit we, integrate the taproot RBF cooperative close
functionality throughout the LND stack. This includes updating
protocol configuration and peer connection handling to support
the new taproot closing flow.

The changes wire through the taproot channel detection, nonce
exchange during shutdown, and proper handling of the enhanced
wire protocol messages in the peer layer. This completes the
integration of taproot RBF cooperative close functionality,
providing a complete alternate closing path for taproot channels
that leverages MuSig2 signatures and implements proper nonce
rotation for secure RBF scenarios.
2026-03-27 14:04:25 -07:00
Olaoluwa Osuntokun
0ce026af20 lnwire: add taproot signatures support to closing_complete message
In this commit we, add support for taproot partial signatures with
nonces to the ClosingComplete message. This is the foundation for
taproot RBF cooperative channel closing, implementing the JIT nonce
pattern required for the modern taproot closing flow.

The changes include a new TaprootClosingSigs struct that mirrors the
existing ClosingSigs but uses PartialSigWithNonce for taproot channels.
The decoding and encoding functions are updated to handle both regular
ECDSA signatures and taproot partial signatures. For taproot channels,
the TaprootClosingSigs field is populated while ClosingSigs remains
empty, maintaining backward compatibility.

We also fix a minor typo in the comment for CloserNoClosee field
(clsoee -> closee).

lnwire: add taproot partial signatures support to closing_sig message

In this commit we, extend the ClosingSig message to support taproot
partial signatures for the RBF cooperative close flow. The ClosingSig
message is sent by the closee in response to a ClosingComplete message.

For taproot channels, we add TaprootPartialSigs which contains partial
signatures without nonces since the remote party already knows our
nonce from the previous ClosingComplete message. We also add a
NextCloseeNonce field for RBF iterations, allowing the closee to
provide a new nonce for the next potential RBF round.

The decoding and encoding functions are updated to handle both regular
signatures and taproot partial signatures, maintaining backward
compatibility with existing non-taproot channels while enabling the
advanced taproot RBF flow.

lnwire: add shutdown nonce support for taproot channels

In this commit we, add support for closee nonces in the Shutdown
message to enable taproot RBF cooperative channel closing. The
ShutdownNonce field allows taproot channels to exchange the initial
nonces required for the MuSig2 signing process during cooperative
closure.

This nonce represents the closee nonce that the remote party will use
when they act as the closer in the RBF flow. The nonce is transmitted
as part of the shutdown flow and is essential for the JIT nonce pattern
used in the modern taproot closing protocol.

The changes maintain backward compatibility as the ShutdownNonce field
is optional and only used for taproot channels that support the
enhanced RBF flow.

lnwire: update test message with taproot signature fields

In this commit we, update the test message utilities to include the new
taproot signature fields added to ClosingComplete and ClosingSig
messages. This ensures the wire protocol tests properly exercise the
new taproot partial signature functionality.

chancloser: add taproot channel infrastructure and nonce state management

In this commit we, add the fundamental infrastructure for taproot RBF
cooperative channel closing. This includes adding taproot channel
detection, MuSig2 session management, and nonce state tracking
throughout the closing state machine.

Key additions include the IsTaproot method on Environment to detect
taproot channels based on the presence of MuSig sessions, and
LocalMusigSession/RemoteMusigSession fields for managing the different
signing contexts. We add NonceState tracking to maintain closee nonces
exchanged during the shutdown phase.

The SendShutdown and ShutdownReceived events are extended to carry
closee nonces for taproot channels, and we add proper error handling
for missing nonces in taproot shutdown messages. These changes provide
the foundation for the taproot-specific state transitions while
maintaining compatibility with existing non-taproot channels.

chancloser: implement taproot cooperative close state transitions

In this commit we, implement the complete taproot RBF cooperative close
state machine transitions. This is a comprehensive change that adds all
the necessary components for taproot channel closing support.

The implementation includes several key areas:

First, we add nonce management helpers including initLocalMusigCloseeNonce
and initRemoteMusigCloseeNonce for properly initializing MuSig2 sessions
with the appropriate closee nonces during the RBF flow.

Second, we implement signature extraction and validation helpers including
partialSigToWireSig for converting partial signatures to wire format, and
extractTaprootSigAndNonce, extractSigAndNonce, and validateAndExtractSigAndNonce
for handling both taproot and regular signatures with proper validation.

Third, we add comprehensive signature encoding logic with encodeClosingSignatures
that creates appropriate signature structures for both channel types, and helper
functions like processRemoteTaprootSig, createLocalCloseeSignature, and
createClosingSigMessage for managing the complex taproot signing flow.

Fourth, we extend the shutdown validation logic to require nonces for
taproot channels and update all state transitions to properly handle
nonce exchange, MuSig2 session initialization, and the dual signature
paths for taproot vs non-taproot channels.

Finally, we add signature preparation logic with prepareClosingSignatures
and extraction helpers like extractSigAndNonceFromComplete that handle
the complex musig signature combination required for taproot channels
while maintaining compatibility with existing ECDSA signatures.

The changes maintain backward compatibility with existing non-taproot
channels while enabling the full taproot RBF cooperative close flow
with proper nonce rotation and signature handling.

chancloser: add taproot test infrastructure and test cases

In this commit we, extend the RBF cooperative close test suite to
support taproot channels. This includes adding schnorr signature
test constants, taproot channel test helpers, and comprehensive
test coverage for the taproot RBF flow.

The changes add localSchnorrSig and remoteSchnorrSig test constants
to mirror the existing ECDSA signatures, and include proper imports
for musig2, chainhash, and lnwallet to support the taproot testing
infrastructure.

The test modifications ensure that both taproot and non-taproot
channels are properly tested throughout the RBF cooperative close
state machine, validating the dual signature handling paths and
nonce management logic introduced in the main implementation.

chancloser: update test utilities and message mapping for taproot

In this commit we, update the chancloser test utilities and message
mapping functions to properly handle the new taproot-specific fields
in the RBF cooperative close flow.

The changes ensure that test harnesses and message mapping functions
are aware of the taproot signature fields and nonce handling required
for the extended wire protocol support. This maintains test coverage
for both existing non-taproot functionality and the new taproot
capabilities.
2026-03-27 14:04:25 -07:00
ziggie
66f64c2b63
routerrpc: implement DeleteForwardingHistory RPC handler
In this commit, we implement the server-side handler for the
DeleteForwardingHistory RPC, connecting the proto definition to the
database layer through the ForwardingLogDB interface on RouterBackend.

The handler resolves the time specification from the request oneof: an
absolute Unix timestamp is used directly, while a relative duration
string is parsed via parseDuration and resolved against the current
clock time. We use the injected clock (RouterBackend.Clock) rather than
time.Now to keep the handler testable.

A configurable minimum age guard (MinFwdHistoryAge, defaulting to 1h)
prevents accidental deletion of recent events. The minimum age can be
overridden via --routerrpc.min-fwd-history-age for environments such as
integration tests that need a shorter threshold.

The context is threaded through to DeleteForwardingEvents so that client
cancellation or deadline expiry aborts the deletion between batches.
Batches already committed at cancellation time are permanent, but the
operation is safe to re-run since deletion is idempotent.
2026-03-27 20:53:00 +01:00
ziggie
67b68096b3
routerrpc: add parseDuration helper for relative time specs
In this commit, we introduce a parseDuration helper that extends Go's
standard time.ParseDuration with additional user-friendly time units:
d (days), w (weeks), M (months, averaged to 30.44 days), and y (years,
averaged to 365.25 days). Fractional values are supported for all units.

All durations must be negative to indicate "time ago" semantics, making
invocations like "-30d" or "-1M" unambiguous at the call site.

The standard library parser is tried first, so all existing Go duration
strings (e.g. "-24h", "-1.5h") continue to work as expected.
2026-03-27 20:52:59 +01:00
ziggie
9f78779079
routerrpc: add DeleteForwardingHistory to Router proto
In this commit, we define the DeleteForwardingHistory RPC in the Router
sub-server protocol and regenerate all derived Go stubs, JSON bindings,
and Swagger documentation.

The RPC uses a oneof for time specification, allowing callers to provide
either an absolute Unix timestamp (delete_before_time) or a relative
duration string (delete_before_duration, e.g. "-30d", "-1M"). The
response includes the count of deleted events and total fees earned in
millisatoshis, allowing operators to maintain financial records while
purging detailed routing surveillance data.
2026-03-27 20:52:59 +01:00
ziggie
1c80f58985
channeldb: add tests for DeleteForwardingEvents
In this commit, we add test coverage for the new DeleteForwardingEvents
method. The tests cover basic deletion, partial deletion by time range,
batch processing across multiple transactions, idempotency, empty
database handling, and exact boundary conditions.

Property-based tests using the rapid package validate key invariants
across randomized inputs: correct event counts, fee calculation
accuracy, time boundary enforcement, and idempotent behaviour.
2026-03-27 20:52:59 +01:00
ziggie
92fea597ca
channeldb: use new for range directive in testcases 2026-03-27 20:52:59 +01:00
ziggie
842c054b02
channeldb: add DeleteForwardingEvents to ForwardingLog
In this commit, we add a new DeleteForwardingEvents method to the
ForwardingLog that allows callers to permanently delete all forwarding
events with a timestamp at or before a specified cutoff time.

The deletion is performed in batches (default 10k, max 50k events per
transaction) to avoid holding large database locks that would block
concurrent operations. Each batch runs in its own transaction, so
other database operations can proceed between batches. Context
cancellation is checked at the start of each batch, allowing callers
to abort mid-way through a large deletion. Any batches already
committed are permanent and will not be rolled back on cancellation.

The method returns a DeleteStats struct containing the number of events
deleted and the sum of fees (AmtIn - AmtOut) earned during that period.
This allows operators to maintain aggregate financial records for
accounting purposes even after purging the detailed event history.
2026-03-27 20:52:59 +01:00
Elle
314f122805
Merge pull request #10672 from ellemouton/fix/private-taproot-v1-funding-script
graph/db: honor taproot feature bit in v1 funding script construction
2026-03-27 17:30:17 +02:00
Elle Mouton
9eac07df31
docs: add release notes for v1 taproot funding script fix 2026-03-27 14:19:21 +02:00
Elle Mouton
0806a27abc
graph/db: honor taproot feature bit in v1 funding script construction
Fix FundingPKScript() to check for the taproot staging feature bit on
v1 channel edges. When present, reconstruct a taproot funding script
via GenTaprootFundingScript instead of the legacy P2WSH multisig.

This is a pre-existing bug: private taproot channels have always been
stored as v1 gossip objects with the taproot feature bit, but
FundingPKScript() never checked for it. The discovery/gossiper layer
(makeFundingScript) already handled this correctly on the insertion
path, but any read path that called FundingPKScript() -- notably
ChannelView() used for chain filter reconstruction on restart --
would produce the wrong script.

Update the tests from the previous commit to assert the correct
taproot funding script instead of the legacy P2WSH script.
2026-03-27 14:19:18 +02:00
Elle Mouton
ad2dc38668
graph/db: add tests to document v1 taproot funding script bug
Private taproot channels are currently represented in the gossip/graph
layer as v1 gossip objects with the SimpleTaprootChannelsRequiredStaging
feature bit set on the v1 ChannelAnnouncement1.

However, FundingPKScript() on a v1 ChannelEdgeInfo unconditionally
reconstructs a legacy 2-of-2 P2WSH multisig script, ignoring the
taproot feature bit entirely. This means that code paths such as
ChannelView() (used to rebuild the chain watch filter on restart)
produce the wrong funding script for these channels.

This bug has always been present since private taproot channels were
first introduced. The discovery/gossiper path (makeFundingScript)
correctly honors the taproot bit when validating announcements on
insertion, but the graph DB read paths never did.

Add failing tests at both the model level (FundingPKScript) and the
graph level (ChannelView round-trip) to document this mismatch. The
next commit fixes the behavior so these tests pass.
2026-03-27 11:57:51 +02:00
Yong
fb1a7e6a4b
Merge pull request #10674 from yyforyongyu/bugfix/10671-bolt1-ping-ignore
lnwire+peer: ignore BOLT 1 no-reply pings
2026-03-27 14:19:27 +08:00
yyforyongyu
59f50fbaad
docs: update release note 2026-03-26 18:55:13 +08:00
yyforyongyu
73770dbf07
peer: include ping pong-size in debug summaries
Expose num_pong_bytes in the ping message summary so ignored no-reply
pings are visible in debug logs. Add a focused test covering the summary
output for the sentinel range.
2026-03-26 18:55:13 +08:00
yyforyongyu
dd61acd9dc
lnwire: remove stale pong limit error
Drop the unused pong limit decode error now that ping deserialization
accepts the full uint16 wire range. Update the randomized ping generator
to cover the full range so the property tests exercise the no-reply
sentinel values too.
2026-03-26 18:55:13 +08:00
yyforyongyu
08b26b6137
lnwire+peer: ignore no-reply pings
Allow pings in the BOLT 1 no-reply range to decode and be ignored
instead of disconnecting peers. This keeps reconnects compatible with
peers that pad channel_reestablish with no-reply pings.
2026-03-26 18:55:13 +08:00
Yong
61f4492e60
Merge pull request #10673 from yyforyongyu/ignore-worktree
gitignore: ignore `.worktrees`
2026-03-26 17:49:25 +08:00
yyforyongyu
331f53beed
gitignore: ignore .worktrees 2026-03-26 17:07:12 +08:00
Yong
671521319c
Merge pull request #10669 from lightningnetwork/native-lint
make: add lint-native target for running linter without Docker
2026-03-26 16:06:56 +08:00
Olaoluwa Osuntokun
c17e66b54f make: add lint-native target for running linter without Docker
In this commit, we add a new `make lint-native` target that builds and
runs the custom golangci-lint binary directly on the host, bypassing
Docker entirely. On macOS in particular, the Docker volume sync overhead
makes `make lint` painfully slow for iterative development.

The native target builds `custom-gcl` from the `tools/` directory using
`go tool golangci-lint custom`, then runs it with `GOWORK=off` and
`--new-from-rev=$(git merge-base HEAD master)` so only changes on the
current branch are linted.
2026-03-25 20:10:28 -07:00
Olaoluwa Osuntokun
46fbc10fc2
Merge pull request #9982 from Roasbeef/splice-nonces
lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels
2026-03-25 16:37:11 -07:00
Olaoluwa Osuntokun
665adade63 lnwallet: add tests for LocalNonces sync 2026-03-25 16:26:22 -07:00
Olaoluwa Osuntokun
3333c84e56 lnwallet+channeldb: integrate LocalNonces in channel sync logic 2026-03-25 16:26:22 -07:00
Olaoluwa Osuntokun
206720dbf5 lnwire: update ChannelReestablish with LocalNonces field
Extends the ChannelReestablish message to include a new optional
LocalNonces field alongside the existing LocalNonce field. This enables
backwards-compatible transmission of multiple nonces for different
purposes during channel reestablishment.

Changes include:
- Add LocalNonces field to ChannelReestablish struct
- Update Encode/Decode methods to handle the new TLV field
- Extend property-based testing to randomly include LocalNonces
- Maintain full backwards compatibility with existing LocalNonce field
2026-03-25 16:26:14 -07:00
Olaoluwa Osuntokun
df29787d98 lnwire: add LocalNonces TLV structure and tests
This commit introduces a new TLV structure LocalNoncesData that contains
a map of transaction IDs to MuSig2 nonces. This structure enables
coordinating multiple nonces for different purposes (e.g., channel
commits, splice operations) within a single wire message.

This is a prep for upcoming spec changes to allow a party that has
in-prorgess splices to tell the remote party which nonces to use for
which splice.
2026-03-25 16:25:48 -07:00
Olaoluwa Osuntokun
9233bbbd1e
Merge pull request #10650 from erickcestari/musig2-validate-nonce-when-decoding
lnwire: validate MuSig2 nonce points on wire decode
2026-03-25 16:21:38 -07:00
ziggieXXX
c14a054604
Merge pull request #10659 from guggero/lncli-wallet-psbt-sign
lncli: add missing `wallet psbt sign` sub command
2026-03-25 16:13:56 +01:00
ziggieXXX
3b598bef3e
Merge pull request #10658 from saubyk/fix_rpc_documentation
Fix rpc documentation for Router Service
2026-03-24 19:27:52 +01:00
ziggieXXX
e4133bcb50
Merge pull request #10065 from ellemouton/asyncGraphCacheLoad
graph/db: async graph cache population
2026-03-24 18:29:44 +01:00
Olaoluwa Osuntokun
a8fb8b4c46
Merge pull request #10660 from ellemouton/fix/peer-ping-nil-deref
peer: fix flaky nil pointer dereference in PingManager
2026-03-24 13:28:41 -04:00
saubyk
06c383f26a
docs: add release notes for missing lncli tags fix 2026-03-24 07:33:29 -07:00
saubyk
0e9748e6a7
routerrpc: add missing lncli tags for RPC documentation
Add lncli: tags to SendPaymentV2, SendToRouteV2, and EstimateRouteFee
proto definitions so the generated API docs correctly show their
corresponding CLI commands (sendpayment, sendtoroute, estimateroutefee)
instead of "There is no CLI command for this RPC".
2026-03-24 07:32:22 -07:00
Yong
bee0d598ce
Merge pull request #10661 from ellemouton/worktree-itest-flake-fix
lntest: disable bitcoind v2 P2P transport in itests
2026-03-24 22:27:47 +08:00
Oli
aee7eb689d
lnrpc: add lncli command hint for API docs generator 2026-03-24 13:13:44 +01:00
Oli
1ed028fc6c
docs: add release notes 2026-03-24 13:13:43 +01:00
Oli
99712e1caf
lncli: add sign subcommand to lncli wallet psbt
Adds a new sub command `lncli wallet psbt sign` to `lncli` that calls
the wallet RPC's `SignPsbt` method.
2026-03-24 13:13:43 +01:00
Elle Mouton
2187d0fdf4
lntest: disable bitcoind v2 P2P transport in itests
bitcoind v29 attempts a v2 P2P handshake when connecting to the btcd
miner, but btcd doesn't support v2 transport. The handshake times out
after 30s before falling back to v1, which consumes the entire
DefaultTimeout budget and causes flakes in tests that rely on timely
block propagation after reconnecting (e.g. open_channel_reorg_test).

Add -v2transport=0 to both the itest chain backend and the bitcoind
miner backend, matching what the unit test backend already does.
2026-03-24 12:15:45 +02:00
Elle Mouton
d4089661a8
peer: add mock BestBlockView to test peer config
The test Config in createTestPeer left BestBlockView nil. When the
PingManager's timer fires during a test, it calls
BestBlockView.BestBlockHeader() which panics on the nil receiver.
This was a flaky failure since it depended on a race between the
timer and test completion.

Add a trivial mockBestBlockView that returns an empty block header
and wire it into the test Config.
2026-03-24 11:49:48 +02:00
Elle Mouton
9a2c4c67c4
peer: fix nil deref in newPingPayload on BestBlockHeader error
The condition guarding the early return used && when it should have
used ||. When BestBlockHeader returns an error with a nil header, the
old code only short-circuited if the nil header equalled
lastBlockHeader. Otherwise it fell through to header.Serialize(),
causing a nil pointer dereference panic.

Change the condition to return the cached serialized header whenever
there is an error OR when the header is unchanged.
2026-03-24 11:49:35 +02:00
Elle Mouton
f733eed26e
docs: add release notes 2026-03-24 09:49:23 +02:00
Elle Mouton
844d0460a1
lnrpc: expose graph cache state in GetInfo
Add a GraphCacheStatus enum to GetInfoResponse so callers can tell
whether the graph cache is disabled, still loading, or fully loaded.

This makes the async graph cache startup state visible to operators and
clients without changing the existing DB fallback behaviour for reads.
2026-03-24 09:49:23 +02:00
Elle Mouton
eb04d405a2
multi: add --db.sync-graph-cache-load option
Add a new option to opt out of the new asynchronous graph cache loading
feature.
2026-03-24 09:49:23 +02:00
Elle Mouton
4486b5261d
graph/db: allow async cache population
Introduce graphCacheState, a wrapper around GraphCache that tracks its
population lifecycle (loading -> loaded) and buffers concurrent mutations
during the initial DB scan. Once population completes, buffered updates
are replayed and the cache begins serving reads.

Start() now launches populateCache in a background goroutine by default.
While the cache is loading, all graph reads fall back to the database.
The KV iterators (ForEachNodeCacheable, ForEachChannelCacheable) now
respect context cancellation so that Stop() can interrupt a long-running
population.

Tests cover: concurrent reads during population, concurrent write replay,
shutdown cancellation during load, population failure with DB fallback,
and KV iterator cancellation.
2026-03-24 09:49:23 +02:00
ziggieXXX
db765a752b
Merge pull request #10481 from bhandras/lntest-bitcoind-miner
lntest: add bitcoind miner backend
2026-03-23 18:17:50 +01:00
Andras Banki-Horvath
ad62f5fa21
docs: add 0.21 release note for bitcoind miner tests 2026-03-23 15:46:38 +01:00
Andras Banki-Horvath
ccb3ec3859
multi: Run itests with bitcoind miner in CI
Add a basic itest matrix entry that sets minerbackend=bitcoind alongside
backend=bitcoind, ensuring CI covers the bitcoind miner path.
2026-03-23 15:46:37 +01:00
Andras Banki-Horvath
df2843485e
multi: Allow selecting miner backend in itest
Add an itest flag to choose the miner backend (btcd vs bitcoind) and
provide a build-tag default so that `-tags=bitcoind` naturally uses a
bitcoind miner.

Wire the flag through `make testing_flags.mk` so callers can set
`minerbackend=bitcoind` independently of the chain backend.
2026-03-23 15:46:37 +01:00
Andras Banki-Horvath
307e12ae99
multi: Add bitcoind miner backend to lntest
Introduce a miner backend interface and implement both btcd and
bitcoind-backed miners for lntest. This lets the harness drive mining
and mempool assertions using bitcoind in addition to btcd.

Also update harness helpers to avoid btcd-only assumptions (network
params, raw tx submission, funding shim output index lookup) and make
bitcoind miner disconnects more reliable.
2026-03-23 15:46:37 +01:00
Elle Mouton
ee4bc4dde5
graph/db: add startup context cancellation
Create a cancellable context in Start() and store its cancel function
on the struct. Stop() invokes it so that long-running DB iterations
(e.g. cache population) can be interrupted promptly during shutdown.
2026-03-23 15:12:57 +02:00
Elle Mouton
24d6e9d618
graph/db: move graph disabled check to inside populateCache
Refactor so that we don't have two layers of indentation later on when
we want to spin populateCache off into a goroutine.
2026-03-23 14:37:33 +02:00
Elle Mouton
19216ee32d
graph/db: add cacheLoaded atomic bool
Use this to block reading from the cache unless cacheLoaded returns
true. This will start being useful once cache population is done
asynchronously.
2026-03-23 14:37:32 +02:00
Elle Mouton
7f7b85ee22
graph/db: misc graphCache test updates
Clean up TestGraphCacheTraversal so that we are explicitly enabling the
graphCache. This removes the need to explicitly make calls to the cache.

Also remove a duplicate check from assertNodeNotInCache.
2026-03-23 14:37:32 +02:00
Elle Mouton
3c25195330
graph/db: don't let tests write to graphCache
Instead of letting tests set the graphCache to nil in order to simulate
it not being set, we instead make use of the WithUseGraphCache helper.
2026-03-23 14:37:32 +02:00
ziggieXXX
02206abe61
Merge pull request #10509 from ziggie1984/cnct-reorg-v2
add-close-confirmation-info
2026-03-19 21:10:08 +01:00
ziggie
86af342d89
routing: fix log output line 2026-03-19 19:30:13 +01:00
ziggie
c455975d23
docs: add release-notes to LND release 21 2026-03-19 19:30:13 +01:00
ziggie
3992dbbeb5
itest: add test for blocks_til_closed field
Add an integration test that verifies the blocks_til_closed field and
the close_height field in the WaitingCloseChannel RPC response.
The test covers:

1. Initial state: shows full required confirmations when tx unconfirmed
2. Countdown: decrements as blocks are mined
3. Reorg handling: resets to full confirmations when close tx is
   reorged out of the chain
4. Recovery: countdown resumes correctly after close tx is re-mined
2026-03-19 19:29:29 +01:00
ziggie
8b4ecf6b70
lnrpc+rpcserver: add new info to WaitingCloseChannel
Add a new fields blocks_til_closed and close_height to the
WaitingCloseChannel message in PendingChannels RPC response.
This shows users how many more blocks until the waiting close
channel will be fully closed and removed.

The required confirmations are determined by CloseConfsForCapacity which
scales based on channel capacity for reorg safety. If the close tx is
not yet confirmed, the full required confirmations are shown.
2026-03-19 19:08:29 +01:00
ziggie
577f25c1c5
contractcourt: track close confirmation height in chain watcher
Update the chain watcher to set and reset the CloseConfirmationHeight
field when monitoring a channel close. When a spend is detected, we
record the spending height so users can see remaining confirmations.
When a reorg removes the close tx from the chain, we reset the height
to 0 to reflect that the transaction is no longer confirmed.
2026-03-19 18:58:45 +01:00
ziggie
09e46297be
channeldb: add CloseConfirmationHeight field to track close tx confirmation
This commit adds a new CloseConfirmationHeight field to the OpenChannel
struct which records the block height at which the closing transaction
was first confirmed. This is stored using TLV encoding (TlvType9) for
backwards compatibility.

A new MarkCloseConfirmationHeight method is added to persist this value,
which can be called when the closing tx confirms and also supports
updates in case of chain reorgs.
2026-03-19 18:58:45 +01:00
ziggieXXX
6ae3c3b882
Merge pull request #10649 from ziggie1984/fix-payments-sql-query
payments/db: fix FilterPayments efficiency and sync bugfixes to migration1
2026-03-19 16:01:23 +01:00
Erick Cestari
7517295ee0
lnwire: validate MuSig2 nonce points on wire decode
Add point-on-curve validation for MuSig2 public nonces at the TLV
decode layer. A MuSig2 nonce is 66 bytes (two 33-byte compressed
secp256k1 public keys). Previously, nonce bytes were accepted without
validation, with invalid points only failing later during MuSig2
session creation deep in the signing flow. Now, malformed nonces from
a peer are rejected immediately at decode time with clear errors.

This hardens all nonce-carrying messages: ClosingComplete (JIT closer
nonces in PartialSigWithNonce), ClosingSig (NextCloseeNonce), Shutdown,
ChannelReestablish, CommitSig, and others.
2026-03-19 11:11:03 -03:00
ziggie
dd73f1e4b0
payments/db/migration1: sync bugfixes from main sql_store
In this commit, we backport two bugfixes from the main sql_store into
the frozen migration1 snapshot that were accidentally missed when
arranging the commits.

The first fix corrects the resolution timestamps for settled and failed
HTLC attempts. The old code passed time.Now() instead of
settleInfo.SettleTime.UTC() and failInfo.FailTime.UTC(), which would
have overwritten the historical timestamps with the current wall clock
on write.

The second fix adds a deterministic sort by SequenceNum after
QueryPayments collects results from the map, ensuring a stable ordering
across calls.

Note that neither SettleAttempt, FailAttempt, nor QueryPayments are
invoked by the migration code itself — the migration only writes
historical KV data into SQL and reads it back for validation. These
changes are included purely for completeness so the frozen snapshot
doesn't silently carry known bugs.
2026-03-19 12:42:26 +01:00
ziggie
845ebe6f48
sqldb+paymentsdb: improve filterpayments efficiency
In this commit, we fix a gap that was introduced when the payment DB
commits were arranged for the main line. The `migration1` package
already had the updated `FilterPayments` query using `COALESCE` for
the index bounds and a separate `FilterPaymentsDesc` query for
reverse-ordered pagination, but the corresponding changes in
`sqldb/sqlc` and the main `sql_store.go` were accidentally left out.

The old `FilterPayments` used OR-based nullable params for the
`created_at` bounds (e.g., `p.created_at >= $3 OR $3 IS NULL`). On
Postgres, mixing nullable text fallbacks in a `COALESCE` with
timestamp columns causes a type mismatch error. The OR-based approach
also prevents the query planner from using the `created_at` index.
We fix this by providing non-nullable `time.Time` params from the Go
side, defaulting to epoch start and year 9999 when no filter is set.

We also drop the `Reverse` param from `FilterPaymentsParams` and
instead introduce a dedicated `FilterPaymentsDesc` query that orders
by `p.id DESC`. This avoids the conditional `CASE WHEN` ordering
trick, which some planners handle poorly.
2026-03-19 12:15:13 +01:00
Olaoluwa Osuntokun
39810d7c35
Merge pull request #10377 from GeorgeTsagk/btcwallet-sign-tweaks
lnwallet: support combined tweak to private key
2026-03-18 12:12:46 -07:00
ziggieXXX
fd6e2b6516
Merge pull request #10647 from ziggie1984/docs/remove-gossiper-deadlock-release-note
docs: remove gossiper deadlock fix entry from 0.21.0 release notes
2026-03-18 17:14:04 +01:00
ziggie
7a2a4ef55c
docs: remove gossiper deadlock fix entry from 0.21.0 release notes
The gossiper deadlock fix (PR #10540) was already included in the
v0.20.1 release, so the entry should not appear again in the 0.21.0
release notes.
2026-03-18 15:07:56 +01:00
Olaoluwa Osuntokun
21ee770e8c
Merge pull request #10533 from saubyk/20.1-releasenotes-fix
docs: Release notes patches for 20.1 and 21.0
2026-03-17 18:32:39 -07:00
saubyk
1306376984
docs: release notes entries removed for prs merged with release v0.20.1 2026-03-17 15:29:08 -07:00
saubyk
84e82bfe98
docs: add missing v0.20.1 release notes from release branch 2026-03-17 15:16:47 -07:00
ziggieXXX
fec556da76
Merge pull request #10645 from ziggie1984/set-sql-store-payments
use sql store in payments
2026-03-17 22:42:33 +01:00
ziggie
4f73b4a070
itest: add a payments db itest similar to invoices and graph 2026-03-17 21:42:24 +01:00
ziggie
f56e933a3e
lnd: add clarifiying comment when introducing a new db 2026-03-17 20:55:23 +01:00
ziggie
8f11732aa3
lnd: use sql store for payments in normal build 2026-03-17 20:55:23 +01:00
Elle
8758151b82
Merge pull request #10633 from ellemouton/waitingproof-type-migration
channeldb+migration: add type-prefixed waiting proof records
2026-03-17 12:31:30 +02:00
Elle Mouton
fd239c18f5
channeldb: add V2 (taproot) waiting proof codec support
Introduce the WaitingProofInner interface and two concrete
implementations — V1WaitingProof (AnnounceSignatures1) and
V2WaitingProof (AnnounceSignatures2 + optional aggregate MuSig2 nonce).

WaitingProof.Encode/Decode now dispatch on the type prefix byte added
in the previous commit, so the store can transparently persist either
proof variant.

The gossiper is updated with a V1 type assertion to maintain existing
behaviour; full V2 gossiper integration will follow when taproot channel
announcements are wired up.

No live code path creates V2 waiting proofs yet — this commit only
lands the codec and storage readiness so the schema is in place before
new writers are introduced.
2026-03-17 11:10:39 +02:00
Elle Mouton
e0256a7d49
channeldb: add type-prefix to waiting proof store records
Existing waiting proof records encode a bare isRemote flag followed by a
raw AnnounceSignatures1 payload. A future gossip v2 implementation will
store AnnounceSignatures2 (taproot) proofs in the same bucket, so each
record needs a discriminator byte to select the correct decoder.

This commit:

 1. Defines WaitingProofTypeV1 (= 0x00) for the current
    AnnounceSignatures1-based proofs.
 2. Updates WaitingProofKey to 10 bytes [proofType(1) || scid(8) ||
    isRemote(1)] to avoid cross-version key collisions.
 3. Adds migration 35, which rewrites every existing record to prepend
    the type byte and rewrites keys to the new format.
 4. Updates WaitingProof.Encode/Decode to always write/expect the prefix.

The migration, codec changes, and tests are kept in one atomic commit so
there is no intermediate revision where the new Decode can encounter
unmigrated records.
2026-03-17 11:05:22 +02:00
Elle
62c931de31
Merge pull request #10582 from ellemouton/g175-db-8
[g175] graph/db: add versioned range queries and complete v2 graph query migration
2026-03-17 10:25:38 +02:00
ziggieXXX
8fb69b20c0
Merge pull request #10611 from calvinrzachman/multi-readonly-middleware
rpcperms: allow multiple read-only middleware
2026-03-17 00:26:02 +01:00
Calvin Zachman
58f5997ef7
docs: update v0.21 release notes 2026-03-16 17:01:01 -04:00
Calvin Zachman
2fa9d92e32
itest: add test for multiple read-only rpc mw 2026-03-16 17:00:55 -04:00
Calvin Zachman
e94e2117b9
rpcperms: allow multiple read-only middleware
There is validation which requires "read-only" middle ware
not specify a caveat name. But when you try to register a second
read-only middleware, there's validation which prevents double
registration for same caveat (though in this case the caveat name
is the empty string).

Update the dameon to permit the registration of multiple
read-only rpc middleware.
2026-03-16 17:00:46 -04:00
Elle Mouton
d47dd3ff6f
docs: update release notes 2026-03-16 11:29:41 +02:00
Elle Mouton
4ae4c70307
graph/db: version ChannelView and add v2 queries
Add a gossip version parameter to ChannelView in the Store interface,
KV and SQL implementations, and the ChannelGraph wrapper. The KVStore
guards v2 requests with ErrVersionNotSupportedForKVDB; the SQLStore
filters by the requested version.

Add three new SQL queries to support version-scoped channel lookups:

  - GetPublicV1ChannelsBySCID: public v1 channels in a SCID range,
    ordered by SCID.
  - GetPublicV2ChannelsBySCID: public v2 channels in a SCID range,
    ordered by SCID.
  - ListChannelsPaginatedV2: paginate v2 channels by internal DB ID,
    used by ChanUpdateRange.

Add TestVersionedDBs/channel_view to verify that v1 and v2 channel
views each return only their respective channels.
2026-03-16 11:29:41 +02:00
Elle Mouton
8a7afc1bac
multi: version ForEachNode, ForEachNodeCached, NumZombies
Add a gossip version parameter to ForEachNode, ForEachNodeCached, and
NumZombies in the Store interface and propagate it through the KV and
SQL implementations and the ChannelGraph wrapper.

The KVStore gates each method against GossipVersion1, returning
ErrVersionNotSupportedForKVDB for any other version. The SQLStore uses
the version to filter the underlying queries.

All call sites—routing graph, autopilot, RPC server, and the graph
migration integration test—are updated to pass the appropriate version
explicitly.
2026-03-16 11:29:41 +02:00
Elle Mouton
50b2567a83
graph/db: add VersionedGraph routing/cache methods
Add version-free shadow methods to VersionedGraph so it satisfies the
routing.Graph, graphdb.NodeTraverser, and related interfaces used by the
channel router and RPC layer.

FetchNodeFeatures and ForEachNodeDirectedChannel delegate to the graph
cache when available, falling back to the store with the baked-in
version. ForEachNode, ForEachNodeCached, ChannelView, and
NodeUpdatesInHorizon all forward to the embedded ChannelGraph with the
version pre-applied.

Update server.go and rpcserver.go to pass s.v1Graph (a *VersionedGraph
wrapping the main graphDB with GossipVersion1) wherever the routing and
session interfaces are needed, replacing direct *ChannelGraph references
that no longer satisfy those interfaces after the version parameters were
added.
2026-03-16 11:29:41 +02:00
Elle Mouton
084bac6e1e
graph/db: version DeleteChannelEdges, IsPublicNode, IsZombieEdge
Propagate the gossip version parameter through DeleteChannelEdges,
IsPublicNode, and IsZombieEdge on ChannelGraph, passing it down to the
underlying Store. Previously these methods hard-coded GossipVersion1
internally; surfacing the parameter lets callers operate on the version
appropriate for the channel.

Also fix two call sites that were still passing *ChannelGraph where a
version-aware interface was expected:

  - rpcserver.go AddInvoice now uses s.v1Graph (a *VersionedGraph) so
    that the invoicesrpc.GraphSource interface—whose IsPublicNode method
    does not take a version parameter—is satisfied.
  - subrpcserver_config.go wraps graphDB in NewVersionedGraph with
    GossipVersion1 when populating the invoicesrpc config Graph field
    via reflection, for the same reason.
2026-03-16 11:29:41 +02:00
Elle Mouton
3c06daed8b
graph/db: version FilterChannelRange
Add a gossip version parameter to FilterChannelRange in the Store interface,
both KV and SQL implementations, and the ChannelGraph wrapper.

KVStore guards against non-v1 versions with ErrVersionNotSupportedForKVDB.
SQLStore accepts any known gossip version, filtering the channel results by
version and using it in policy lookups. The SQL query still uses
GetPublicV1ChannelsBySCID for now (a TODO marks where a version-aware query
will be substituted in a follow-up).

VersionedGraph.FilterChannelRange shadows the ChannelGraph method with a
version-free signature, passing its baked-in version to the store. This keeps
the ChannelGraphTimeSeries interface and ChanSeries implementation unchanged.

Add TestFilterChannelRangeVersionGuard to verify that the KV store returns
ErrVersionNotSupportedForKVDB for v2 requests while the SQL store handles
them gracefully.
2026-03-16 11:29:40 +02:00
Elle Mouton
23288dce6f
graph/db: version MarkEdgeZombie
Add a gossip version parameter to MarkEdgeZombie in the Store interface,
both KV and SQL implementations, and the ChannelGraph wrapper, following
the same pattern established for MarkEdgeLive.

KVStore guards against non-v1 versions with ErrVersionNotSupportedForKVDB.
SQLStore accepts any known gossip version and uses it in the UpsertZombieChannel
call and cache invalidation.

Builder.MarkZombieEdge (the ad-hoc path for validation failures) passes
GossipVersion1 as all channels in that path are v1.
2026-03-16 11:29:40 +02:00
Elle Mouton
cb99637f0d
graph,discovery: version IsZombieChannel
Change the IsZombieChannel / isStillZombieChannel function signature
throughout the gossip and routing stacks from
func(time.Time, time.Time) bool to func(ChannelUpdateInfo) bool.

This allows zombie detection to inspect the full channel update info—
including version and freshness type—rather than receiving two raw unix
timestamps that carry no meaning for v2 channels.

Builder.IsZombieChannel is updated to extract version-appropriate
freshness from the ChannelUpdateInfo: unix-time expiry for v1, and a
block-count expiry (derived from ChannelPruneExpiry and
avgBitcoinBlockTime) for v2. The gossipSyncer, SyncManager, and gossiper
Config fields are updated to use the new signature.
2026-03-16 11:29:40 +02:00
Elle Mouton
2a9a431516
graph/db: rework ChannelUpdateInfo to use lnwire.Timestamp
Replace the separate Node1UpdateTimestamp/Node2UpdateTimestamp (time.Time)
and Node1BlockHeight/Node2BlockHeight (uint32) fields in ChannelUpdateInfo
with a unified Node1Freshness/Node2Freshness pair typed as lnwire.Timestamp.

The lnwire.Timestamp interface (added in the previous commit) is either a
UnixTimestamp (v1) or BlockHeightTimestamp (v2), making it structurally
impossible to pass block-height values into a v1 constructor or vice versa.

Two version-specific constructors replace the old single constructor:
  - NewV1ChannelUpdateInfo(scid, node1Time, node2Time time.Time)
  - NewV2ChannelUpdateInfo(scid, node1BlockHeight, node2BlockHeight uint32)

Add Node1FreshnessTime/Node2FreshnessTime helper methods on ChannelUpdateInfo
to extract the underlying time.Time from a UnixTimestamp, which the discovery
syncer needs for its v1-only isStale/isSkewed and isStillZombieChannel checks.

All call sites in kv_store, sql_store, graph_test, and syncer are updated
accordingly.
2026-03-16 11:25:32 +02:00
Elle Mouton
50dd86400e
lnwire: add Timestamp interface
Add a Timestamp interface for channel and node update ordering values
that abstracts over the two freshness semantics used by the gossip
versions:

  - UnixTimestamp (uint64): seconds-since-epoch, used by v1 gossip
    channels and nodes.
  - BlockHeightTimestamp (uint32): block height, used by v2 gossip
    channels and nodes.

Both concrete types implement IsZero() and Cmp(). Cmp returns an error
if the two operands are of different concrete types, preventing
accidental cross-version comparisons.
2026-03-16 11:25:31 +02:00
Elle Mouton
08823ac079
graph/db: version FilterKnownChanIDs callback
Change the isZombieChan callback in FilterKnownChanIDs (and its
ChannelGraphTimeSeries interface counterpart) from
func(time.Time, time.Time) bool to func(ChannelUpdateInfo) bool.

This allows callers to make version-aware zombie decisions using the full
ChannelUpdateInfo—including freshness type—rather than two raw time.Time
values that are meaningless for v2 channels.

The GossipSyncer adapts its v1-only isStillZombieChannel check by
wrapping it in a closure that extracts Node1/Node2FreshnessTime from the
ChannelUpdateInfo. All other call sites are updated accordingly.
2026-03-16 11:25:31 +02:00
Elle Mouton
e9eff6297f
graph/db: version MarkEdgeLive
Add a gossip version parameter to MarkEdgeLive throughout the stack:

- Store interface and KVStore/SQLStore implementations now take
  lnwire.GossipVersion; KVStore rejects non-v1 with
  ErrVersionNotSupportedForKVDB, SQLStore uses the version in the
  DeleteZombieChannel query and cache invalidation.
- ChannelGraph.MarkEdgeLive passes the version through to both the
  store call and the FetchChanInfos cache repopulation.
- FilterKnownChanIDs uses GossipVersion1 explicitly for its internal
  MarkEdgeLive call; this site will be properly versioned when
  FilterKnownChanIDs itself is versioned.
- graph.ChannelGraphSource interface and Builder.MarkEdgeLive updated
  accordingly.
- Discovery gossiper and test mock updated to pass GossipVersion1 at
  their (v1-only) call sites.
2026-03-16 11:25:31 +02:00
Elle Mouton
787dbcf7b3
graph: make zombie detection version-aware
Introduce isPolicyZombie to handle version-specific channel staleness.
For v1 policies, staleness is measured by wall-clock time since the last
update (unchanged behaviour). For v2 policies, staleness is measured by
the number of blocks elapsed since LastBlockHeight, using
avgBitcoinBlockTime to convert the configured prune expiry into an
equivalent block count.

isZombieChannel is simplified to call isPolicyZombie for each edge and
inline the strict/non-strict pruning logic directly, removing the prior
indirect call through IsZombieChannel.
2026-03-16 11:25:31 +02:00
ziggieXXX
f67c3463a7
Merge pull request #10627 from ziggie1984/promote-payments-migrations
sqldb: move native SQL payments migrations into mainline
2026-03-16 09:01:46 +01:00
ziggie
17da81d678
docs: add release-notes for LND 21 2026-03-15 22:03:57 +01:00
ziggie
04aeca44a9
sqldb: move native SQL payments migrations into mainline
The native SQL payments migrations were previously gated behind test
build tags in migrations_dev.go. This commit promotes them into the
main migration sequence in migrations.go, making them available in
production builds.

The following migrations are moved to mainline:
- 000010_payments (v12): initial payments SQL schema
- 000011_payment_duplicates (v13): duplicate payment support
- kv_payments_migration (v14): optional KV to SQL payment migration
- 000012_drop_redundant_invoice_indexes (v15): index cleanup
- 000013_payments_index_improvements (v16): payment index optimizations
2026-03-13 20:30:43 +01:00
ziggieXXX
11e249b134
Merge pull request #10535 from ziggie1984/kvdb-sql-payments-improvements
improve speed of retrieval of payments
2026-03-13 18:50:30 +01:00
ziggieXXX
0c4f01c064
Merge pull request #10643 from ziggie1984/move-coop-close-release-note
docs: move coop close confirmation scaling note to 0.21.0
2026-03-13 14:53:45 +01:00
ziggie
291e445676
docs: update release notes
The previous release-note entry was not super accurrate so we
update it to reflect the change in more detail.
2026-03-13 13:56:12 +01:00
ziggie
3657fb4bcc
docs: move coop close confirmation scaling note to 0.21.0
In this commit, we move the release note for the improved confirmation
scaling for cooperative closes (PR #10331) from the 0.20.1 release notes
to 0.21.0, where the change actually landed.
2026-03-13 13:56:12 +01:00
ziggie
cb36d0d977
sqldb: fix error comparison and refactor sqlite bench helpers
Replace direct `err != sql.ErrNoRows` comparison with `errors.Is` and
extract the repeated fetch-and-check logic into a helper to reduce
duplication across the sequential and concurrent benchmarks.
2026-03-12 18:24:05 +01:00
ziggie
bd18a1e2d6
docs: add release-notes 2026-03-12 18:24:05 +01:00
ziggie
80b3ecebd6
paymentsdb: fix typo in comment 2026-03-12 18:24:05 +01:00
ziggie
e0ea1f9da3
payments/db: add percentage and rolling window ETA to migration progress
Before this change the migration progress log only showed absolute counts
and a cumulative average rate. This adds two improvements:

- A cheap pre-count pass over the payments index bucket before migration
  starts, giving an upper-bound estimate of the total entries to migrate
  (noted as approximate since duplicates are also indexed). This allows
  showing a percentage complete on each progress line.

- A rolling 300s window rate for the ETA calculation instead of the
  cumulative average rate. The cumulative rate is slow to react when
  throughput changes mid-migration; the rolling window makes the ETA
  responsive to recent conditions. On window reset the previous window's
  rate is used as a fallback for one tick to avoid a gap in the ETA.

Example progress line after this change:

  Progress: 500000 payments (~48.5%), 499860 attempts | Rate: 399.2
  pmt/s | Elapsed: 20m50s | ETA: ~22m8s
2026-03-12 18:24:05 +01:00
ziggie
50205ffe1b
payments/db: fix format in log message
fixes it for the orignal file and the code migration package where
the code was just copied over.
2026-03-12 18:24:05 +01:00
ziggie
c5866b978b
sqldb: scope DeleteFailedAttempts query to payment's own attempts
The previous query used an IN subquery that scanned all failed
resolutions across all payments (O(N) where N = total failed attempts
globally). Replace with a correlated EXISTS subquery that only checks
resolutions for the specific payment's attempts, making it O(k) where
k = attempts for this payment (typically 1-5).
2026-03-12 18:24:05 +01:00
ziggie
3a15d7e917
multi: add omit_hops option to ListPayments RPC
Add a new omit_hops field to ListPaymentsRequest that allows clients
to skip loading hop-level route data for HTLC attempts, reducing both
query cost and response size. When set, the route is returned with
only route-level fields (TotalTimeLock, TotalAmount, SourcePubKey)
and no individual hop data or hop-level custom records.
2026-03-12 18:24:03 +01:00
ziggie
3e7dfff07e
sqldb: optimise payment index layout in new migration 13
Since migration 10 is already merged into master it cannot be edited.
Add a new migration (000013_payments_index_improvements) that carries
forward two index improvements:

 - Drop idx_htlc_attempt_index on payment_htlc_attempts(attempt_index)
   and idx_route_hops_htlc_attempt_index on
   payment_route_hops(htlc_attempt_index). Both are redundant with
   existing UNIQUE constraints and only add write/maintenance overhead.

 - Add idx_htlc_payment_id_attempt_time on
   payment_htlc_attempts(payment_id, attempt_time) to optimise batched
   attempt reads that filter by payment_id and order by attempt_time
   (FetchHtlcAttemptsForPayments).

 - Add idx_htlc_resolutions_type_attempt_index on
   payment_htlc_attempt_resolutions(resolution_type, attempt_index) to
   optimise the failed-attempt cleanup path that filters by
   resolution_type before joining on attempt_index
   (DeleteFailedAttempts).
2026-03-12 18:23:25 +01:00
ziggie
3e61931825
docs: add removal notice of deprecated hop rpc elements
The previous commit stopped setting the channel capacity when
retrieving the route. This commit makes sure that in the next
release we remove the entries from the rpc interface.
2026-03-12 18:23:25 +01:00
ziggie
21fa0710cf
routerrpc: dont query for the channel capacity
During route retrieval don't query for the channel capacity. We
default to the static incomingAmt of the route. That was already
done previously when the channel was closed or private. The
channel capacity has been deprecated for quite a while so it is
acceptable to avoid the performance hit querying the graph db.

In the next release this field will be removed.
2026-03-12 18:23:25 +01:00
ziggie
c2e8618f3a
multi: remove payment tombstone
The invoice tombstone acts as a system wide kv db tombstone so
there is no need for a specific payment tombstone. Moreover a
TODO is added to redesign the current setting of the invoice
tombstone because it is also fragile to crashes after
the tombstone is set and the sql transaction of the migration
fails to commit.

Additionally the missing cleanup calls are added in case we return
early because of an error.
2026-03-12 18:23:24 +01:00
Olaoluwa Osuntokun
78b104ec71
Merge pull request #9639 from Roasbeef/taproot-final-scripts
input: update taproot scripts to add an option for the miniscript compat versions from latest spec
2026-03-11 20:46:19 -05:00
Olaoluwa Osuntokun
8a5d241a4a input: add template vs builder byte-for-byte equivalence tests
Add regression tests that compare every ScriptTemplate-based function
against the original ScriptBuilder implementations extracted from git
history. This ensures the template migration produces identical script
bytes for all 22 script types (segwit v0 + taproot).

The legacy builder functions are kept as private test helpers in
script_utils_legacy_test.go, extracted verbatim from the pre-template
commit.
2026-03-11 19:36:09 -05:00
Olaoluwa Osuntokun
833f17fe6b input: use template to unify taproot "prod script" definition 2026-03-11 19:21:13 -05:00
Olaoluwa Osuntokun
506391145a input: add cut out for final taproot scripts from spec
In this commit, we add an initial cut out to allow us to swap in the
taproot scripts (which changed slightly) once we start using the final
"production" feature bit.

The changes to the scripts are pretty mechanic: we avoid using `OP_DROP`
and instead use a `_VERIFY` earlier in the script to consume the stack
item.
2026-03-11 19:21:13 -05:00
Olaoluwa Osuntokun
f26ad67912 input: switch to using new txscript.ScriptTemplate function
In this commit, we switch to using the new txscript.ScriptTemplate
function. This allows us to write the script in plain text, using some
hidden template operations to swap in items like keys or sigs.

This reduces in less code and boiler plate over all, the code that
defines the script now reads as if it was a comment.
2026-03-10 20:32:17 -05:00
Olaoluwa Osuntokun
d3deeb45f7 build: update to latest btcd version 2026-03-10 20:32:17 -05:00
Olaoluwa Osuntokun
f57054cec6
Merge pull request #10629 from lightningnetwork/bump-neutrino-btcwallet-deps
mod+multi: bump neutrino to v0.16.2 and btcwallet to 70a94ea
2026-03-10 20:02:07 -05:00
Olaoluwa Osuntokun
54688752f3 ci: update pinned grpc dependency to v1.79.1
In this commit, we update the CI workflow pinned dependency check
for google.golang.org/grpc from v1.59.0 to v1.79.1. While btcwallet
only requires v1.73.0, we upgrade to the latest stable release for
bug fixes and improvements.
2026-03-10 17:41:56 -05:00
Olaoluwa Osuntokun
07f2d1aa06 lnrpc: regenerate protobuf definitions for grpc v1.79.1
In this commit, we regenerate all protobuf Go stubs to match the
updated grpc and protobuf library versions. This is a mechanical
change with no functional differences; the generated code simply
uses the newer protobuf runtime APIs.
2026-03-10 17:41:56 -05:00
Olaoluwa Osuntokun
ae286f278f mod: bump google.golang.org/grpc to v1.79.1
In this commit, we bump the grpc dependency from the v1.73.0 version
(pulled in transitively by btcwallet) to v1.79.1 which is the latest
release and includes several bug fixes. This also bumps a number of
related transitive deps including google.golang.org/protobuf to v1.36.10
and several golang.org/x packages.
2026-03-10 17:23:48 -05:00
Olaoluwa Osuntokun
641ff68c03 mod+multi: bump neutrino to v0.16.2, btcwallet to 70a94ea39e9c
In this commit, we update our two core chain backend dependencies:
neutrino is bumped from v0.16.1 to v0.16.2, and btcwallet is updated
to commit 70a94ea39e9c (a pre-release past v0.16.17).

The updated btcwallet changes the `chain.Interface` `Start` method
signature from `Start() error` to `Start(context.Context) error`. The
same change was made to neutrino's `ChainService.Start`. We update all
call sites and interface implementations across the codebase to pass a
`context.Background()` at non-test call sites, and `t.Context()` in
tests.

The affected packages are: chainntnfs/bitcoindnotify, chainreg,
config_builder, lnmock, lntest/unittest, lnwallet/btcwallet,
lnwallet/test, and routing/chainview.
2026-03-10 17:19:29 -05:00
Yong
c30dff768c
Merge pull request #10637 from gijswijs/fix-linter-post-10089-merge
Fix linter issue in brontide.go
2026-03-09 20:39:54 +08:00
Yong
d344984dbb
Merge pull request #10613 from saubyk/update-getdebuginfo-rpc
Update getdebuginfo rpc
2026-03-09 17:49:04 +08:00
Gijs van Dam
e8074935d9
chore: fix linter issues in brontide.go
Post merge of #10089, a linter issues was introduced in `brontide.go`.
This commit fixes that issue.
2026-03-09 10:41:14 +01:00
Yong
4c5c792e8e
Merge pull request #10461 from Euler-B/doc/updatePaytoPaywithDocker
Update  step-by-step send payment guide with docker
2026-03-09 17:26:00 +08:00
saubyk
92c0f55f1e
docs: add release notes for GetDebugInfo include_log flag 2026-03-07 12:51:06 -08:00
saubyk
94bb665087
cmd/commands: add --include_log flag to getdebuginfo CLI
Add an --include_log boolean flag to the getdebuginfo and
encryptdebugpackage commands. When set, the log file content is included
in the response. The encryptdebugpackage command description is updated
to reflect the new default behavior.
2026-03-07 12:51:00 -08:00
saubyk
0e80b24c89
rpcserver: skip log reading when include_log flag is not set
By default, GetDebugInfo now returns only the configuration map. The log
file is only read and included when the include_log flag is set to true,
avoiding the cost of reading large log files when only config data is
needed.
2026-03-07 12:50:51 -08:00
saubyk
f0e228733d
lnrpc: add include_log field to GetDebugInfoRequest
Add an `include_log` bool field to GetDebugInfoRequest proto message.
When set to true, the server will include the log file content in the
response in addition to the configuration map.
2026-03-07 12:50:41 -08:00
Olaoluwa Osuntokun
392d4c8cb3
Merge pull request #10089 from gijswijs/onion-messaging-1
Onion message forwarding
2026-03-06 11:46:38 -06:00
ziggieXXX
6ea4791895
Merge pull request #10635 from ziggie1984/worktree-bw-compat-log-collection
scripts/bw-compat: fix log collection
2026-03-05 20:49:30 +01:00
Eduardo Bravo
b3225ce3d4
docs: add release-notes 2026-03-05 12:15:30 -04:00
ziggie
2b4dc4eace
scripts/bw-compat: use docker logs for log collection
Switch collect_logs from docker cp to docker logs to reliably capture
container output. The previous approach used docker cp to copy lnd log
files from inside named volumes, which silently fails in CI — the
directory gets created and the success echo prints, but no files are
ever copied, causing upload-artifact to report "No files were found".

docker logs reads directly from Docker's captured stdout/stderr buffer,
bypassing the volume entirely, and works as long as the container exists.
2026-03-05 15:51:59 +01:00
Eduardo Bravo
dba80c63d7
docs(docker): update README for improved clarity and accuracy 2026-03-05 09:53:28 -04:00
ziggieXXX
7c38c1ea05
Merge pull request #10434 from GeorgeTsagk/extra-aux-band-check
Add `AuxHtlcValidator`
2026-03-05 11:27:07 +01:00
Olaoluwa Osuntokun
cd755cda0c
Merge pull request #9637 from Roasbeef/chan-type-required
feature: start to set the require bit for channel_type
2026-03-04 17:40:59 -08:00
ziggieXXX
1e433db0ee
Merge pull request #10626 from ziggie1984/fix-bench-get-invoice
sqldb: fix GetInvoice call in benchmarks
2026-03-04 20:50:46 +01:00
George Tsagkarelis
8d30e7d160
peer: add test for createHtlcValidator 2026-03-04 20:19:43 +01:00
George Tsagkarelis
1edf8123e4
lnwallet: add aux test utils
We add this constructor for an AuxHtlcDescriptor that allows setting
some of the internal fields. This is useful for testing purposes for
code external to this package that may need to extensively test the
AuxHtlcView.
2026-03-04 20:19:43 +01:00
George Tsagkarelis
b1701cf232
peer: set and use aux htlc validator
When instantiating the lightning channel we now pass in the created HTLC
validator. This validator simply performs a bandwidth check and errors
out if that is insufficient.
2026-03-04 20:02:07 +01:00
George Tsagkarelis
773b2c1c30
lnwallet: add and use AuxHtlcValidator to lightning channel
Previously we'd perform aux bandwidth checks during path finding. This
could lead to issues where multiple HTLCs where querying the same
bandwidth but were not accounting for each other before being added to
the commitment log. We now add a new validator function that will serve
as the last point of checks before adding the HTLC to the commitment.

During path finding HTLCs could query channel bandwidth asynchronously.
At this new call site all HTLCs that are about to be added to the
channel have been organised in sequence, so it's safe to query bandwdith
again at this point as we're getting the actual up-to-date values.

We remove the aux bandwidth check from the helper canSendHtlc, which was
called from CheckHTLCTransit and CheckHTLCForward (both are methods of
the htlcswitch).

For forwards we now fail at the link level, following the introduction
of the AuxHtlcValidator.

For payments, we now may fail either at the pathfinding level, or at the
link level. The htlcswitch may no longer fail for aux bandwidth checks.

Finally, when fetching the latest htlc view (for bandwidth checks during
pathfinding) we'd silently set the nextHeight of the view to the default
zero value. We now make sure to set it to the correct nextHeight value.
2026-03-04 20:02:06 +01:00
ziggie
6750738137
sqldb: fix GetInvoice call in benchmarks
GetInvoice was removed and replaced with GetInvoiceByHash,
GetInvoiceByAddr, etc. Update the sqlite benchmarks to use
GetInvoiceByHash instead.
2026-03-04 19:38:25 +01:00
ziggieXXX
4a3b8029cf
Merge pull request #10619 from ziggie1984/increase-settling-speed
invoices/sql: fix full table scans on HTLC settlement hot path
2026-03-04 18:16:01 +01:00
ziggieXXX
8ec11635ed
Merge pull request #10565 from ziggie1984/fix-sqlite-config-defaults
sqldb+lncfg: wire up unused SQLite config fields
2026-03-04 18:14:34 +01:00
ziggie
d767c1e76d
docs: add release-notes 2026-03-04 13:04:27 +01:00
ziggie
e751d96c13
sqldb: remove unused GetInvoice query 2026-03-04 13:04:26 +01:00
ziggie
7995e87b91
invoices: use longer timeout for HTLC resolution in AMP test
Under parallel Postgres test load, the event loop must complete
sequential DB writes after SetTime fires. Using testTimeout (5s)
is too tight; switch the three blocking hodlChan selects in
testFailPartialAMPPayment to testTimeoutLong (1 minute) to
match the pattern already used elsewhere for slow backends.
2026-03-04 13:04:26 +01:00
ziggie
f6f6064938
scripts: fix backwards compatibility test timing issues
The backwards compatibility test was failing intermittently due to two
related timing issues in the test setup.

The issue was that Dave's `wait_graph_sync dave 3` was hanging
for up to 60 minutes. Dave's initial gossip sync with Charlie could
complete before Charlie had forwarded the alice-bob channel
announcement, leaving Dave stuck at 2 channels until lnd's historical
syncer fired at its default interval of 1 hour. After this 1-hour idle,
some routing state had become stale, causing the subsequent payment from
alice to dave to fail with FAILURE_REASON_NO_ROUTE.

This issues is now addressed by setting `--historicalsyncinterval=10s` on
all nodes. This causes nodes to periodically re-sync the full gossip
state from their peers every 10 seconds instead of every hour. Dave
therefore picks up any missed channel announcements and routing policies
within seconds, and alice's routing graph stays up-to-date throughout
the test.

Additionally, lnd debug logs from all containers are now collected
before teardown on failure and uploaded as a CI artifact, making future
failures easier to diagnose.
2026-03-04 13:04:26 +01:00
ziggie
e2dfd8f034
invoices: add TODO to change the return type of the query 2026-03-04 13:04:26 +01:00
ziggie
0ca69e6f57
invoices/sql: replace catch-all GetInvoice with indexed lookups
The GetInvoice query used an OR IS NULL pattern for each filter
parameter:

  WHERE (i.hash = $1 OR $1 IS NULL)
    AND (i.payment_addr = $2 OR $2 IS NULL)

SQLite's query planner decides on an execution plan at prepare time,
before seeing any parameter values. Because either condition can be
trivially true when its parameter is NULL, the planner conservatively
falls back to a full table scan rather than using the unique indexes on
hash and payment_addr. This caused every invoice lookup and update on
the hot path (HTLC settlement) to scan the entire invoices table.

Replace the single catch-all query with three dedicated queries, each
using a direct equality on a uniquely constrained column:

- GetInvoiceByHash:  WHERE hash = $1
- GetInvoiceByAddr:  WHERE payment_addr = $1  (new, covers AMP path)
- GetInvoiceBySetID: existing, unchanged

Update getInvoiceByRef to route to the appropriate query based on which
fields are present in the InvoiceRef. When both hash and payment address
are provided, we look up by hash and then verify the returned invoice's
payment address matches.

GetInvoice is now unused and removed from the SQLInvoiceQueries
interface.
2026-03-04 13:04:26 +01:00
ziggie
65f6d75511
sqldb: drop redundant and unused invoice indexes
Remove four indexes from the invoices table that either duplicate existing
UNIQUE constraint indexes or are never used in WHERE clauses:

- invoices_hash_idx: redundant, UNIQUE constraint on hash already creates
  an implicit index
- invoices_payment_addr_idx: redundant, UNIQUE constraint on payment_addr
  already creates an implicit index
- invoices_preimage_idx: unused, preimage is NULL on all new invoices and
  is never used as a query filter
- invoices_settled_at_idx: unused, settled_at is NULL on all pending
  invoices and is never used as a query filter (settle_index is used
  instead)

Dropping these reduces B-tree working set size, which improves page cache
utilization as the invoice table grows.
2026-03-04 13:04:26 +01:00
ziggie
de06c29ae9
sqldb: add benchmark for sqlite connection number
The results suggests that the default of 2 is a good conservative
approach. The higher the connection number and the higher the
workload the performance decreases in WAL mode since readers still
need coordination (WAL index shm for example).
2026-03-03 19:23:12 +01:00
Gijs van Dam
061b89a482 docs: release notes for v0.21.0 2026-03-02 15:46:21 +01:00
Gijs van Dam
73e0983802 onionmessage: add LRU cache to SCID resolver
Add an LRU cache to GraphNodeResolver to avoid repeated database lookups
when resolving SCIDs to node public keys. The cache stores up to 1000
compressed pubkey entries, which is sufficient for typical onion message
forwarding scenarios.

This change also introduces a NewGraphNodeResolver constructor to
properly initialize the cache, replacing direct struct literal usage.
2026-03-02 15:46:21 +01:00
Gijs van Dam
aef47fd667 multi: add --protocol.no-onion-messages flag
This commit adds a configuration flag to disable onion messaging support.
When set, lnd will:
- Not advertise the onion messages feature bit (39) in init and node
announcements
- Skip creating the OnionEndpoint at server startup
- Not register an onion message handler with peers, so incoming onion
messages are not processed
2026-03-02 15:46:21 +01:00
Gijs van Dam
ba27627a70 multi: actor-based onion message forwarding
Add onion message forwarding capability using the OnionPeerActor for
communication. Messages are routed through a receptionist pattern where
each peer has a dedicated OnionPeerActor for handling message sends.

The OnionEndpoint uses the sphinx router for decoding and decrypting the
onion message packet and the encrypted recipient data in the payload of
the onion messages.
2026-03-02 15:46:21 +01:00
Gijs van Dam
f464fb9cd8 multi: OnionPeerActor for per-peer message support
Introduce a fat OnionPeerActor that handles the full onion message
processing pipeline for each peer connection. The actor decodes incoming
onion messages, determines the routing action (forward or deliver),
executes the action via PeerMessageSender, and dispatches updates to
subscribers via OnionMessageUpdateDispatcher.

Key components:
- OnionRouter interface abstracting sphinx router operations
- PeerMessageSender interface for forwarding to other peers
- OnionMessageUpdateDispatcher interface for subscriber notifications
- OnionActorFactory for spawning per-peer actors with shared deps
- Full test suite calling Receive() directly with NoOpReplayLog
2026-03-02 15:46:21 +01:00
Gijs van Dam
50b34e96ff multi: add sphinx router without replay protection
Initialize a sphinx router without persistent replay protection logging
for onion message processing. Onion messages don't require replay
protection since they don't involve payment routing.
2026-03-02 15:46:21 +01:00
Gijs van Dam
68cdb4e325 record: add func for non-final om route data
Adds the NewNonFinalBlindedRouteDataOnionMessage function to create
blinded route data specifically for onion messages.
2026-03-02 15:46:21 +01:00
Gijs van Dam
14024fc8e6 go: update lightning-onion
Update lightning-onion to commit that includes onion-messaging support.
2026-03-02 15:46:21 +01:00
Gijs van Dam
6a37f78b72 lnwire: add OnionMessagePayload
The new wire message defines the OnionMessagePayload, FinalHopPayload,
ReplyPath, and related TLV encoding/decoding logic.
2026-03-02 15:46:21 +01:00
ziggieXXX
2aec8f3380
Merge pull request #10485 from ziggie1984/migration-kvdb-sql-payments-part1
payments: kv-to-sql migration (tests + wiring)
2026-03-02 10:29:46 +01:00
ziggie
89f046a129
docs: add release notes for KV-to-SQL payment migration 2026-02-27 09:25:42 +01:00
ziggie
6c28cc4d0b
payments/migration1: freeze lnwire and record dependency
Also freeze the lnwire and record packages used by the migration.

Copy the minimal subset of lnwire files (16) into
payments/db/migration1/lnwire/ and all record files (6) into
payments/db/migration1/record/. Three lnwire files are trimmed to avoid
pulling in the full message-type dispatch tree — all changes are purely
subtractive and can be verified with:

  diff lnwire/message.go payments/db/migration1/lnwire/message.go
  diff lnwire/writer.go  payments/db/migration1/lnwire/writer.go
  diff lnwire/lnwire.go  payments/db/migration1/lnwire/lnwire.go

All migration1 files now import only the frozen packages, removing the
live dependency on lnwire and record so future changes to those packages
cannot affect migration correctness.
2026-02-27 09:25:21 +01:00
ziggie
00025ef3bf
payments/db: normalize LegacyPayload flag in migration comparison
LegacyPayload was a hint used exclusively by the KV store to decide
how to serialize and deserialize the hop payload (legacy format vs
TLV). The SQL store does not serialize hop data at all — every hop
field is persisted natively in its own column — so this flag has no
meaning there and is never stored.

Clear LegacyPayload for all hops inside normalizePaymentForCompare so
that deep-equality checks between KV and SQL payments succeed even when
the KV source data carries LegacyPayload=true.

A dedicated test (TestMigrationLegacyPayloadNormalized) is added to
verify that a payment with LegacyPayload=true hops migrates and
compares correctly.
2026-02-26 17:38:33 +01:00
Elle
8126b424fd
Merge pull request #10610 from ellemouton/graph-cleanup-todo-contexts
graph/db+refactor: surface all `context.TODO()`s
2026-02-26 16:18:31 +02:00
Yong
9bef04a67d
Merge pull request #10604 from lightningnetwork/elle-payment-sql-series-new
payments: SQL backend implementation series
2026-02-26 10:52:57 +08:00
ziggie
cf3610abb9
payments/db: split migration queries into dedicated SQLMigrationQueries interface 2026-02-25 18:52:34 +01:00
ziggie
a2c36d1667
sqldb: add migration consistency test
Tests that all migration files follow the defined schema and that
there are not duplicates which could cause collision.
2026-02-25 18:52:34 +01:00
ziggie
932fbc33f0
graph/db/migration1: fix defer commit/rollback in test tx executor
The defer closure checked a local err variable for commit/rollback
decisions, but err remained nil after a successful BeginTx. When
txBody failed, the error was returned directly without assigning to
err, so the defer always committed instead of rolling back.

Additionally, since err was not a named return value, the defer's
Commit error assignment was silently swallowed.

Replace the error-prone defer pattern with explicit rollback on
txBody failure and a direct Commit return.
2026-02-25 18:52:34 +01:00
ziggie
9def124380
payments/db: fix silent error in duplicate payment lookup
When duplicatePaymentSequenceKey is missing from a duplicate payment
sub-bucket, the code returned the outer function's err variable which
is nil at that point. This caused corrupted duplicate entries to be
silently treated as "not found" instead of failing loudly.

Return a new dedicated ErrNoDuplicateSequenceNumber error so malformed
data is detected immediately.
2026-02-25 18:52:34 +01:00
ziggie
c862e70148
mod: update new direct dependency via go mod tidy 2026-02-25 18:52:33 +01:00
ziggie
89685de16e
payments/migration1: handle legacy payments with nil HTLC hash
For legacy payments, the HTLC Hash field may be nil in the bbolt
backend. Previously, the migration would fail with "HTLC attempt X
missing payment hash" when encountering such payments.

This commit fixes the migration by falling back to the parent payment
hash when the HTLC-specific hash is nil. This is consistent with how
the router handles legacy payments (see patchLegacyPaymentHash in
payment_lifecycle.go).

The validation logic is also updated to apply the same fallback when
comparing bbolt data with migrated SQL data, ensuring the comparison
succeeds.
2026-02-25 18:52:33 +01:00
ziggie
f174b60b94
payments/migration1: wire KV→SQL migration in the main pkg
Hook the payments KV→SQL migration into the SQL migration config.
The migration is still only available when building with the build tag
"test_native_sql".

Moreover a tombstone protection similar to the invoice migration is added
to prevent re-runningi with the KV backend  once migration completes.
2026-02-25 18:52:33 +01:00
ziggie
4b30bed0ac
payments/migration1: add external migration test
Add a developer-facing migration_external_test that allows
running the KV→SQL payments migration against a real channel.db
backend to debug migration failures on actual data. The accompanying
testdata README documents how to supply a database file and configure
the test, so users can validate their data and confirm the migration
completes successfully.

The test is skipped by default and meant for manual diagnostics.
2026-02-25 18:52:33 +01:00
ziggie
f058f3329d
payments/migration1: add migration test suite and helpers
Add test helpers plus sql_migration_test coverage for KV→SQL migration.
Basic migration, sequence ordering, data integrity, and feature-specific cases
(MPP/AMP, custom records, blinded routes, metadata, failure messages). Also
cover duplicate payment migration to payment_duplicates, including missing
attempt info to ensure terminal failure is recorded.

This gives broad regression coverage for the migration path and its edge-cases.
2026-02-25 18:52:33 +01:00
ziggie
a9747207f6
payments/migration1: add the payments mig code
Implement the KV→SQL payment migration and add an in-migration
validation pass that deep-compares KV and SQL payment data in batches.
Duplicate payments are migrated into the payment_duplicates table,
and duplicates without attempt info or explicit resolution are marked
failed to ensure terminal state. Validation checks those rows as well.
2026-02-25 18:52:32 +01:00
ziggie
fb705bb0f9
payments/migration1: freeze core payment code
Copy the core payments/db code into payments/db/migration1 and
add the required sqlc-generated types/queries from sqldb/sqlc.
This effectively freezes the migration code so it stays robust
against future query or schema changes in the main payments package.

Replace the delegation to channeldb.ReadElement/WriteElement with
self-contained, frozen implementations that only handle the exact types
required by this migration package. This removes the dependency on the
live channeldb codec so that future changes to channeldb serialization
cannot silently corrupt or break the migration.

UnknownElementType is also defined locally for the same reason.
2026-02-25 18:52:32 +01:00
ziggie
74f8f2d9e6
sqldb+payments: add payment_duplicates for legacy duplicate payments
Older LND versions could create multiple payments for the same hash.
We need to preserve those historical records during KV→SQL migration,
but they don’t fit the normal payment schema because we enforce a
unique payment hash constraint. Introduce a lean payment_duplicates
table to store only the essential fields (identifier, amount,
timestamps, settle/fail data).

This keeps the primary payment records stable and makes the migration
deterministic even when duplicate records lack attempt info. The table
is intentionally minimal and can be dropped after migration if no
duplicate payments exist.

For now there is no logic in place which allows the noderunner to
fetch duplicate payments after the migration.
2026-02-25 18:52:32 +01:00
ziggie
2596d34619
paymentsdb+sqldb: add migration related query
Add a migration specific query which allows to set the failure
reason when inserting a payment into the db.
2026-02-25 18:52:31 +01:00
ziggie
e9a88267ff
paymentsdb: fix duplicate interface check and down migration drop order
- Remove duplicate compile-time interface assertion for SQLStore.

- Fix the down migration to drop payment_intents before payments to
respect the foreign key dependency order. This was not a bug in the
first place bc we have the CASCADE when deleting payments.
2026-02-25 18:36:14 +01:00
ziggie
216de55dff
paymentsdb: sort FetchInFlightPayments result by sequence number
The SQL implementation collects payments into a map before converting
to a slice, resulting in non-deterministic iteration order due to Go's
intentional map randomisation. Sort the result by SequenceNum to produce
a deterministic, insertion-ordered output.

Note that the current sole caller (resumePayments in router.go) processes
each payment independently, so this ordering does not affect any existing
behaviour.
2026-02-25 18:36:14 +01:00
ziggie
0c2951aa05
paymentsdb: fix SettleAttempt and FailAttempt to use caller-provided timestamps
The SQL backend introduced in this PR was ignoring the SettleTime and
FailTime fields provided in HTLCSettleInfo and HTLCFailInfo, instead
always recording time.Now() as the resolution timestamp. The KV backend
correctly serializes and deserializes these fields.

The timestamps are set by the caller using a mockable clock
(p.router.cfg.Clock.Now() in payment_lifecycle.go), so ignoring them
means the stored timestamp reflects when the DB write happened rather
than when the event occurred, breaking deterministic testing.

This commit also extends the test assertions in assertPaymentInfo to
verify that SettleTime and FailTime are correctly stored and retrieved
by the SQL backend, and updates the relevant call sites to pass explicit
timestamps so regressions are caught.
2026-02-25 18:36:14 +01:00
Elle Mouton
262e9208b9
graphdb: thread topology update context 2026-02-25 16:11:47 +02:00
Elle Mouton
0bb0d66952
graphdb: pass context to GraphSession 2026-02-25 16:11:46 +02:00
Elle Mouton
8722a96a4e
graphdb: pass context to IsClosedScid 2026-02-25 16:11:46 +02:00
Elle Mouton
4dcaf1c16a
graphdb: pass context to PutClosedScid 2026-02-25 16:11:46 +02:00
Elle Mouton
68c5206017
graphdb: pass context to AddEdgeProof 2026-02-25 16:11:46 +02:00
Elle Mouton
3dc2efd7f3
graphdb: pass context to DisconnectBlockAtHeight 2026-02-25 16:11:46 +02:00
Elle Mouton
a31c86b0ee
graphdb: pass context to PruneTip 2026-02-25 16:11:46 +02:00
Elle Mouton
9f855175d5
graphdb: pass context to ChannelView 2026-02-25 16:11:46 +02:00
Elle Mouton
7dbaa691c4
graphdb: pass context to PruneGraph 2026-02-25 16:11:46 +02:00
Elle Mouton
13668ce45f
graphdb: pass context to PruneGraphNodes 2026-02-25 16:11:46 +02:00
Elle Mouton
072244ee18
graphdb: pass context to FilterKnownChanIDs 2026-02-25 16:11:46 +02:00
Elle Mouton
46d37a691f
graphdb: pass context to FetchChanInfos 2026-02-25 16:11:46 +02:00
Elle Mouton
f62ea62cfe
graphdb: pass context to IsPublicNode 2026-02-25 16:11:42 +02:00
Elle Mouton
397330cbdb
graphdb: pass context to ChannelID 2026-02-25 16:11:29 +02:00
Elle Mouton
3079c07afb
graphdb: pass context to HasChannelEdge 2026-02-25 16:11:29 +02:00
Elle Mouton
87ed09a829
graphdb: pass context to HasV1ChannelEdge 2026-02-25 16:11:29 +02:00
Elle Mouton
1d4c6aadb4
graph/db: thread context through FetchChannelEdgesByOutpoint 2026-02-25 16:11:29 +02:00
Elle Mouton
6bbb9a32fc
graph/db: thread context through FetchChannelEdgesByID 2026-02-25 16:11:25 +02:00
Elle Mouton
acdef84d30
graph/db: thread context through DeleteChannelEdges 2026-02-25 15:33:59 +02:00
Elle Mouton
37cc6b29d9
graph/db: thread context through NumZombies 2026-02-25 15:33:34 +02:00
Elle Mouton
f98905ae99
graph/db: thread context through IsZombieEdge 2026-02-25 15:33:34 +02:00
Elle Mouton
8a96e5f3d2
graph/db: thread context through MarkEdgeLive 2026-02-25 15:33:34 +02:00
Elle Mouton
c270367071
graph/db: thread context through MarkEdgeZombie 2026-02-25 15:33:34 +02:00
Elle Mouton
bb38aa8922
graph/db: thread context through FilterChannelRange 2026-02-25 15:33:34 +02:00
Elle Mouton
e8e714f6a3
graph/db: thread context through ForEachChannelCacheable 2026-02-25 15:33:34 +02:00
Elle Mouton
dc9dae8dd6
graph/db: thread context through ChanUpdatesInHorizon 2026-02-25 15:33:34 +02:00
Elle Mouton
6ccf93c2c9
graph/db: thread context through ForEachNodeDirectedChannel 2026-02-25 15:33:34 +02:00
Elle Mouton
2f0d2962dd
graph/db: thread context through NodeUpdatesInHorizon 2026-02-25 15:33:34 +02:00
Elle Mouton
0597250ae4
graph/db: thread context through DisabledChannelIDs 2026-02-25 15:33:33 +02:00
Elle Mouton
b1873e63ec
graph/db: thread context through FetchNodeFeatures 2026-02-25 15:32:05 +02:00
Elle
13a8356558
Merge pull request #10609 from ellemouton/graph-unit-test-cleanup
graph: replace fatal assertions and simplify test helpers
2026-02-25 14:11:13 +02:00
Elle Mouton
160c2a9f68
graph/db tests: make compareEdgePolicies assert internally
Convert compareEdgePolicies into a test helper that accepts a
testing handle and performs assertions directly.

Update call sites to invoke the helper instead of threading errors
into immediate require.NoError checks.
2026-02-25 11:34:31 +02:00
Elle Mouton
ceb939ebb1
graph/tests: make createChannelEdge assert internally
Update createChannelEdge to take a testing handle, mark itself as a
helper, and call require.NoError for funding script generation.

All call sites now consume only returned values and no longer plumb
an immediately-asserted error value.
2026-02-25 11:34:05 +02:00
Elle Mouton
963975135c
graph/tests: make randEdgePolicy assert internally
Update randEdgePolicy to take a testing handle, mark itself as a
helper, and assert internal packing errors directly with require.

Call sites now receive only the policy value without plumbing an
error through immediate require.NoError checks.
2026-02-25 11:32:04 +02:00
Elle Mouton
8dfe747c02
graph: migrate tests from Fatal to require helpers
Replace remaining fatal-style assertions in graph package tests with
direct testify/require helpers. This simplifies control flow and makes
test intent clearer by using NoError, True/False, Len, Empty, and
Equal/EqualValues assertions.

Also remove FailNow-style patterns in favor of specific assertion
helpers.
2026-02-25 11:31:28 +02:00
Elle Mouton
f8899dd324
graph/db: migrate tests from Fatal to require helpers
Replace remaining fatal-style assertions in graph/db tests with direct
testify/require helpers. This simplifies control flow and reduces
indentation by using NoError, True/False, Equal, Len, Empty, and Nil
assertions directly.
2026-02-25 11:14:55 +02:00
ziggie
a2cb753428
sqldb: rename 000009_payments to 000010_payments
The 000009 schema version slot is now taken by 000009_graph_v2_columns
which was merged ahead of the payments schema. Bump the payments schema
file number to 000010 to avoid the collision.

Moreover update the migration_dev.go file to reflect this change and
update the order of migration.
2026-02-25 08:02:27 +01:00
ziggie
2548c15167
docs: add release-notes 2026-02-25 08:02:27 +01:00
ziggie
372fbbd70b
paymentsdb: add more unit tests to increase coverage
We add a couple of additional tests to increase the unit test
coverage of the sql store but also the kv store. We only create
db agnostic unit tests so both backends are tested effectively.
2026-02-25 08:02:26 +01:00
ziggie
a0a22b7ff9
paymentsdb: add additional test for first hop data
We add a test which tests the retrieval of first hop data like
the first hop amount or the custom records on the route level.
2026-02-25 08:02:26 +01:00
ziggie
a861ac8d46
paymentsdb: refactor test helpers 2026-02-25 08:02:26 +01:00
ziggie
995ad7e1b5
multi: move failed attempt cfg option to the router subsytem
Previously we had db and application logic mixed on the db level.
We now move the config option KeepFailedPaymentAttempts to the
ChannelRouter level and move it out of the db level.
2026-02-25 08:02:26 +01:00
ziggie
253e4fd832
routing: add TODO to also delete payments without HTLCs 2026-02-25 08:02:26 +01:00
ziggie
3ae0ccc6f5
paymentsdb: make delete payments test db agnostic
We make the TestDeleteNonInFlight and separate all the logic out
for the duplicate payment test case. The deletion of duplicate
payments is now tested in isolation only for the kv backend.
2026-02-25 08:02:26 +01:00
ziggie
9da39e5655
docs: add release-notes 2026-02-25 08:02:25 +01:00
ziggie
d8d5d3f990
multi: thread context through SendPayment 2026-02-25 08:02:25 +01:00
ziggie
4cc5428087
routing: add context to reloadPayment method 2026-02-25 08:02:25 +01:00
ziggie
03d891d52d
routing: add context to reloadInflightAttempts 2026-02-25 08:02:25 +01:00
ziggie
5b06854850
routing: add context to failAttempt 2026-02-25 08:02:25 +01:00
ziggie
e130ccc494
routing: Thread context through failPaymentAndAttempt
A context is added to failPaymentAndAttempt and its dependant
function calls.
2026-02-25 08:02:25 +01:00
ziggie
8038dc0054
multi: thread context through payment lifecyle functions 2026-02-25 08:02:25 +01:00
ziggie
dfe0c43c7d
routing: Add context to requestRoute 2026-02-25 08:02:24 +01:00
ziggie
33597ad35d
docs: add release notes 2026-02-25 08:02:24 +01:00
ziggie
7f2df4b088
multi: thread context through DeleteFailedAttempts 2026-02-25 08:02:24 +01:00
ziggie
635a67c94a
multi: thread context through Fail payment functions 2026-02-25 08:02:24 +01:00
ziggie
446300984e
multi: thread context through FailAttempt 2026-02-25 08:02:24 +01:00
ziggie
3b7fdabaec
multi: thread context through SettleAttempt 2026-02-25 08:02:24 +01:00
ziggie
8bb7150dc2
multi: thread context through RegisterAttempt method 2026-02-25 08:02:24 +01:00
ziggie
60026494b1
multi: thread context through InitPayment 2026-02-25 08:02:23 +01:00
ziggie
10062ca9ac
multi: thread context through FetchInflightPayments 2026-02-25 08:02:23 +01:00
ziggie
25d05be038
multi: thread context through FetchPayment 2026-02-25 08:02:19 +01:00
ziggie
563a2ac9d3
multi: thread context through DeletePayments 2026-02-25 08:02:19 +01:00
ziggie
bc15d4bb50
multi: thread context through DeletePayment 2026-02-25 08:02:19 +01:00
ziggie
fcb8929ea5
docs: add release-notes 2026-02-25 08:02:19 +01:00
ziggie
dfacb9ec00
paymentsdb: add unit test for FetchInflightPayments method 2026-02-25 08:02:18 +01:00
ziggie
e40c8df16b
paymentsdb: add firstcustom records to unit tests 2026-02-25 08:02:18 +01:00
ziggie
bfd59aac22
paymentsdb: add more comments 2026-02-25 08:02:18 +01:00
ziggie
ea4e183c39
lnrpc: fix linter 2026-02-25 08:02:18 +01:00
ziggie
f518931cf0
itest: fix list_payments accuracy edge case 2026-02-25 08:02:18 +01:00
ziggie
5f1f413596
paymentsdb: make specific kv store tests only available via build tag 2026-02-25 08:02:18 +01:00
ziggie
080fd78fb4
paymentsdb: introduce a harness interface
The design of the sql and kv db are a bit different. A harness
interface is introduced which allows us to unit most of the test
and keep the backend specific tests at a minimum.
2026-02-25 08:02:17 +01:00
ziggie
570863ca26
paymentsdb: add harness to run payment db agnostic tests
In commit adds the harness which will be used to run db agnostic
tests against the kv and sql backend. We have adopted all the
unit tests so far so that with this commit all the payment tests
not specifically put into the kv_store_test.go should all pass
for all backends.
2026-02-25 08:02:17 +01:00
ziggie
2eadbfb54d
paymentsdb: fix test case before testing sql backend
We are now not supporting the LegacyPayload for the onion packet
anymore. All payments and their onion payload need to be tlv
encoded. The sql backend assumes tlv so we have to always set the
in memory presentation of a hop where the legacy parameter is still
available but deprecated to false, otherwise the hops will not be
equal and unit tests for the sql backend will fail when switched
on in the next commits.
2026-02-25 08:02:17 +01:00
ziggie
de50f471e8
paymentsdb: make QueryPayments test db agnostic
We make the QueryPayments test db agnostic and also keep a small
test for querying the duplicate payments case in the kv world.
2026-02-25 08:02:17 +01:00
ziggie
fdfa1430f8
paymentsdb: refactor test helpers
Since now the sql backend is more strict in using the same
session key we refactor the helper so that we can easily change
the session key for every new attempt.
2026-02-25 08:02:17 +01:00
ziggie
9ad3f2195b
paymentsdb: remove kvstore from sql db implementation
Now that every method of the interface was implemented we can
remove the embedded reference we put into place for the sql store
implementation so that the interface would succeed. This is now
removed.
2026-02-25 08:02:17 +01:00
ziggie
c919d25619
docs: add release-notes 2026-02-25 08:02:16 +01:00
ziggie
4e52896cc1
paymentsdb: added unit test for computePaymentStatusFromResolutions 2026-02-25 08:02:16 +01:00
ziggie
969b00e4a5
paymentsdb: implement FetchInFlightPayments for sql backend 2026-02-25 08:02:16 +01:00
ziggie
82ee78c5ef
paymentsdb: use batch function when querying for resolutions 2026-02-25 08:02:16 +01:00
ziggie
b02531cc85
paymentsdb: rename functions and variables
We take inspiration from the graph sql implementation and name
the variables accordingly.
2026-02-25 08:02:16 +01:00
ziggie
218a30d16e
paymentsdb: add a wrapper to the fetchpayment method
We wrap the fetchPayment db call and catch the case where no errors
are found in the db, where we now return the ErrPaymentNotInitiated
error.
2026-02-25 08:02:16 +01:00
ziggie
ff12082452
paymentsdb: implement DeletePayments for sql backend 2026-02-25 08:02:16 +01:00
ziggie
53a7877af0
paymentsdb: implement FailAttempt for sql backend 2026-02-25 08:02:15 +01:00
ziggie
fd6796e561
multi: implement Fail method for sql backend 2026-02-25 08:02:15 +01:00
ziggie
caf73c59da
paymentsdb: rename paymentsBatchData
We rename this variable to paymentsDetailsData because we will
also need to batch load the core payment and intent data in
future commits and this renaming should make it clear that this
does match payment related data but not the core data which is
in the payment and in the intent table.
2026-02-25 08:02:15 +01:00
ziggie
5d328c976e
paymentsdb: remove pointer receiver dependecy to make it more robust
We remove the SQLStore from most of the helper functions. This
also makes sure we do not accidentally create a new db tx but use
the provided db SQLQueries parameter.
2026-02-25 08:02:15 +01:00
ziggie
7d87c0ce2f
paymentsdb: fix formatting for sql QueryPayments 2026-02-25 08:02:15 +01:00
ziggie
4c867c1b88
docs: add release-notes 2026-02-25 08:02:15 +01:00
ziggie
620dacc432
paymentsdb: implement SettleAttempt for sql backend 2026-02-25 08:02:14 +01:00
ziggie
c0e55b5855
paymentsdb: verify total amount for last hop in the blinded path 2026-02-25 08:02:14 +01:00
ziggie
40c8502fd3
paymentsdb: implement RegisterAttempt for sql backend 2026-02-25 08:02:14 +01:00
ziggie
d2cdd9d86f
paymentsdb: add note to RegisterAttempt 2026-02-25 08:02:14 +01:00
ziggie
510f6fb532
paymentsdb: implement InitPayment for sql backend 2026-02-25 08:02:09 +01:00
ziggie
0c96a2d722
sqldb+paymentsdb: add queries to insert all relavant data
In this commit we add all queries which we will need to insert
payment related data into the db.
2026-02-25 08:02:09 +01:00
ziggie
c3e25ff8ef
paymentsdb: implement DeletePayment for sql backend 2026-02-25 08:02:09 +01:00
ziggie
3012eb2ca0
paymentsdb: implement DeleteFailedAttempts for sql backend 2026-02-25 08:02:09 +01:00
ziggie
6faf68c5fa
paymentsdb: add query to only fetch resolution type for HTLCs 2026-02-25 08:02:08 +01:00
ziggie
bdea68bebf
sqldb: add queries for deleting a payment and attempts 2026-02-25 08:02:08 +01:00
ziggie
27071acb6b
sqldb: Change payment_intent relationship to payment table
Previously a one(intent)-to-many(payment) relationship it is now
changed to a one-to-one relationship because a payment request
only can have 1 payment related to it. Looking into the future
with BOLT12 offers, the fetched invoice from the offer could be
stored here as well and the relationship would still hold.
2026-02-25 08:02:08 +01:00
ziggie
f8241748a1
paymentsdb: enhance some godoc function descriptions 2026-02-25 08:02:08 +01:00
ziggie
4c5eaad606
docs: add release-notes 2026-02-25 08:02:08 +01:00
ziggie
8aae9ffc2c
paymentsdb: implement FetchPayment for sql backend 2026-02-25 08:02:08 +01:00
ziggie
e8fe45fe65
paymentsdb: implement QueryPayments for sql backend 2026-02-25 08:02:08 +01:00
ziggie
eeca189b87
paymentsdb: add new internal error 2026-02-25 08:02:07 +01:00
ziggie
18e7768837
multi: add relevant queries for QueryPayments implemenation 2026-02-25 08:02:04 +01:00
ziggie
e7a3096621
sqldb: add index and comment to payment tables 2026-02-25 08:01:16 +01:00
ziggie
b6a05f7951
docs: add release-notes 2026-02-25 08:01:15 +01:00
ziggie
8c7b2367e5
lnd: make the payment schema migration available for testing
We allow the migration of the payment schema to be applied when
the test_native_sql is active to make sure the tables are properly
contructed.
2026-02-25 08:01:15 +01:00
ziggie
dd585a821b
sqldb: add payment sql tables
This does not include duplicate payments yet. They will be added
when the migration code is introduced for payments.
2026-02-25 08:01:15 +01:00
ziggie
93626822f7
lnd+paymentsdb: introduce harness for the payment sql backend
We prepare the code for the sql payment backend. However no
payment db interface method for the sql backend is implemented
yet. This will be done in the following commits. They currently
use the embedded KVStore to satify the build environment.
2026-02-25 08:01:14 +01:00
Olaoluwa Osuntokun
0b00c66231
Merge pull request #10601 from ziggie1984/invoice-filter-index-optimization
invoices/sql_store: replace catch-all FilterInvoices with targeted index-friendly queries
2026-02-24 16:34:27 -08:00
Yong
344af33ac7
Merge pull request #10557 from Liongrass/master
Remove redundant documentation from /docs
2026-02-24 21:17:22 +08:00
ziggieXXX
c81d69e56a
Merge pull request #10586 from GeorgeTsagk/add-george-key
scripts: add gpg key for georgetsagk
2026-02-23 16:01:52 +01:00
George Tsagkarelis
05d1df6de1
scripts: add gpg key for georgetsagk 2026-02-23 14:40:07 +01:00
Elle
dd65ba2b01
Merge pull request #10543 from bitromortac/2602-channel-update-events
rpc: add channel updates to SubscribeChannelEvents
2026-02-23 13:09:36 +02:00
ziggie
1e5e6f27ef
docs: add release notes for invoice SQL query performance improvements 2026-02-21 17:05:44 +01:00
ziggie
d26bf84843
invoices/sql_store: add tests for targeted invoice query variants
Add testFetchPendingInvoicesAccepted to explicitly verify the state
filtering behaviour of FetchPendingInvoices across all four contract
states:

  ContractOpen     (state 0) – must be returned
  ContractAccepted (state 3) – must be returned
  ContractSettled  (state 1) – must be excluded
  ContractCanceled (state 2) – must be excluded

This directly exercises the `state IN (0, 3)` predicate introduced in
the FetchPendingInvoices SQL query and addresses the review request for
explicit test coverage of the new targeted query variants. The test
runs against the KV, SQLite, and Postgres backends.
2026-02-21 17:05:44 +01:00
ziggie
9becdfa8a3
sqldb/sqlc: remove deprecated FilterInvoices query
All call sites have been migrated to the targeted replacement queries
in the previous commit. Remove FilterInvoices and its associated
params struct from the SQL source and regenerate.
2026-02-21 16:58:31 +01:00
ziggie
774ebaf126
invoices/sql_store: switch to targeted queries, default timestamp bounds
Replace all four FilterInvoices call sites with the focused queries
introduced in the previous commit:

  FetchPendingInvoices   → FetchPendingInvoices
  InvoicesSettledSince   → FilterInvoicesBySettleIndex
  InvoicesAddedSince     → FilterInvoicesByAddIndex
  QueryInvoices          → FilterInvoicesForward / FilterInvoicesReverse

The first three are straight 1:1 swaps — the new params structs carry
only the fields that are actually used, and the removed fields (Reverse,
PendingOnly, unused index bounds) were always left at their zero values.

QueryInvoices is restructured more substantially. The forward/reverse
branch now selects between FilterInvoicesForward and
FilterInvoicesReverse, each of which takes a concrete id bound that is
always set:

  forward: AddIndexGet = IndexOffset + 1   (≥ 1 when IndexOffset = 0)
  reverse: AddIndexLet = IndexOffset - 1   (or MaxInt64 when offset = 0)

Timestamp parameters are changed from nullable (sql.NullTime with
OR-based SQL fallbacks) to always-on Go-side defaults, consistent with
the approach used by the payments query:

  createdAfter  → time.Unix(0, 0).UTC()          (epoch, before any invoice)
  createdBefore → time.Date(9999, 12, 31, …)     (far future, no upper cap)

This ensures the planner always sees plain range predicates on
created_at and can use the invoices_created_at_idx index.

The SQLInvoiceQueries interface is updated to expose the five new
methods and drop FilterInvoices.
2026-02-21 16:57:08 +01:00
ziggie
866efbd7fb
sqldb/sqlc: add targeted invoice queries
The existing FilterInvoices query uses optional parameters via the
pattern `(col >= param OR param IS NULL)` for every filter. SQLite
cannot use indexes with this pattern because the OR prevents the query
planner from determining at plan time which rows satisfy the condition,
resulting in a full table scan regardless of the available indexes
(invoices_state_idx, invoices_settle_index_idx, and the primary-key
clustered index on id).

Additionally, the conditional ORDER BY:

  ORDER BY CASE WHEN reverse = FALSE ... THEN id ELSE NULL END ASC,
           CASE WHEN reverse = TRUE  ... THEN id ELSE NULL END DESC

prevents the planner from using the index ordering and forces an
explicit sort.

Add five focused replacements, each with a plain sargable predicate and
a direct ORDER BY so the planner can always choose an index scan:

  - FetchPendingInvoices:        WHERE state IN (0, 3)
  - FilterInvoicesBySettleIndex: WHERE settle_index >= $1
  - FilterInvoicesByAddIndex:    WHERE id >= $1
  - FilterInvoicesForward:       WHERE id >= $1 ... ORDER BY id ASC
  - FilterInvoicesReverse:       WHERE id <= $1 ... ORDER BY id DESC

FilterInvoicesForward and FilterInvoicesReverse accept non-nullable
timestamp parameters (created_after, created_before) so the planner
always sees plain range predicates on created_at. Callers supply
Go-side defaults when no date filter is needed, following the same
convention already used by the payments query.

FilterInvoices is kept in this commit so all existing call sites
continue to compile. It will be removed once all callers have been
migrated.
2026-02-21 16:09:03 +01:00
bitromortac
3a00ed4a8d
docs: update release notes 2026-02-20 10:47:48 +01:00
bitromortac
fa5722355f
itest: test channel update subscription 2026-02-20 10:47:04 +01:00
bitromortac
10e8a69cec
lnd: handle channel update events in SubscribeChannelEvents 2026-02-20 10:47:04 +01:00
bitromortac
4f22a5130d
proto: add ChannelCommitUpdate to channel event updates 2026-02-20 10:47:03 +01:00
bitromortac
4701f00cfd
htlcswitch: let link notify on remote commit sig 2026-02-20 10:43:52 +01:00
bitromortac
0f1472536c
peer+htlcswitch: inject notification endpoint 2026-02-20 10:43:52 +01:00
bitromortac
a3d87f16b9
channelnotifier: let notifier take channel updates 2026-02-20 10:43:52 +01:00
bitromortac
19860bb9f6
lnwallet: expose channel state
We create a deep copy of the channel state as we want to later expose
the data structure to the rpc, which already has helper methods to
marshal this representation to the rpc representation.
2026-02-20 10:43:52 +01:00
Elle
78b2036f39
Merge pull request #10572 from ellemouton/g175-db-7
[g175] graph/db: add gossip-version plumbing for core graph APIs
2026-02-20 10:26:06 +02:00
Elle Mouton
09e20dc7fa
docs: update release notes 2026-02-20 09:28:13 +02:00
Elle Mouton
7c7a0ae13d
graph/db: fetch policy version in cache paginated query
The ListChannelsWithPoliciesForCachePaginated query was missing the
policy version column, causing extractChannelPolicies to hardcode
lnwire.GossipVersion1 for that row type. Add cp1.version and
cp2.version to the query and use the fetched values instead.
2026-02-20 09:28:13 +02:00
Elle Mouton
acb3240d3d
graph/db: version fetch chan infos
Add a gossip version parameter to FetchChanInfos in the Store interface
and both KV/SQL implementations. Update the graph builder caller and
refactor related tests.
2026-02-20 09:28:13 +02:00
Elle Mouton
769d6260f5
graph/db: version highest chan id
Add a gossip version parameter to HighestChanID in the Store interface
and both KV/SQL implementations. Update callers in the discovery
ChanSeries and server bootstrap code.
2026-02-20 09:28:13 +02:00
Elle Mouton
95d6af12d4
graph/db: version batched edge and cache node channel tests
Convert testBatchedAddChannelEdge and testGraphCacheForEachNodeChannel
to run against both v1 and v2 gossip versions.
2026-02-20 09:19:24 +02:00
Elle Mouton
fc19a24d07
graph/db: version disabled channel IDs
Add a gossip version parameter to DisabledChannelIDs in the Store
interface and both implementations. Add a new version-filtered SQL
query and update the builder caller.
2026-02-20 09:19:23 +02:00
Elle Mouton
672b999d22
graph/db: version edge info and policy update tests
Convert testEdgeInfoUpdates and testBatchedUpdateEdgePolicy to run
against both v1 and v2 gossip versions, exercising the versioned
Store methods added in prior commits.
2026-02-20 09:19:23 +02:00
Elle Mouton
28fe3585ac
graph/db: version ChannelID lookup
Add a gossip version parameter to the ChannelID method (outpoint to
short channel ID lookup) in the Store interface and both KV/SQL
implementations. Update the VersionedGraph wrapper and tests.
2026-02-20 09:19:22 +02:00
Elle Mouton
2f6cc185a7
graph/db: version createChannelEdge
Refactor the createChannelEdge test helper to be version-aware,
supporting both v1 and v2 channel and policy creation. This prepares
the test infrastructure for subsequent commits that version individual
Store methods.
2026-02-20 09:19:20 +02:00
Elle Mouton
0b1a1e3dc5
graph/db: version ForEachNodeDirectedChannel and cacheable traversal
Add a gossip version parameter to ForEachNodeDirectedChannel on the
Store interface and both DB implementations (KVStore, SQLStore). The
NodeTraverser and routing.Graph interfaces remain unversioned since
pathfinding operates on the merged cross-version cache view.

The cache population in populateCache is updated in the same commit
because it is logically atomic with the versioning changes: the graph
cache is the unversioned, merged view used by pathfinding, so it must
be populated with data from all gossip versions. Without this change,
only v1 data would be loaded into the cache, making v2 nodes and
channels invisible to pathfinding.
2026-02-20 08:50:46 +02:00
Elle Mouton
f5c91691bc
graph/db: version cacheable iteration methods
Add gossip version parameters to ForEachNodeCacheable and
ForEachChannelCacheable in the Store interface and both implementations.

Thread the version through SQL helpers and update call sites/tests
accordingly.
2026-02-20 08:50:43 +02:00
Elle Mouton
d48a16036d
graph/db: version fillTestGraph test helper
Make fillTestGraph version-aware and update its call sites to pass an
explicit gossip version (currently v1 at these call sites).

This is a test-helper refactor only; cacheable-iteration API versioning
is handled in the next commit.
2026-02-20 08:48:42 +02:00
Elle Mouton
898e4ef854
graph/db: add gossip version aliases
Introduce gossipV1 and gossipV2 package-level aliases in sql_store.go
to reduce verbosity in version switch statements. Leave hard-coded
v1/v2 call sites untouched so remaining upgrades are obvious.

In other words, it is now easy to see where our remaining work in the
sql_store.go file is by just searching for instances of
`lnwire.GossipVersion1`.
2026-02-20 08:46:59 +02:00
Elle Mouton
3ea2423f25
graph/db: version ForEachSourceNodeChannel
Add a gossip version parameter to ForEachSourceNodeChannel in the Store
interface and both KV/SQL implementations. The VersionedGraph wrapper
delegates with its baked-in version. Convert the
testAddChannelEdgeShellNodes and testForEachSourceNodeChannel tests to
run against both v1 and v2 gossip versions.
2026-02-20 08:46:58 +02:00
Elle Mouton
2aaa52adf9
graph/db: version channelCache by gossip version
Add a channelCacheKey struct keyed by {GossipVersion, chanID}, matching
the pattern already used by rejectCache. This prevents v1 and v2
channel data from colliding in the shared cache.

All callers in KVStore (always GossipVersion1) and SQLStore (version
from context) are updated to pass the version parameter.

This will be needed for later on when we update methods that use this
cache to be versioned (like ChannelUpdatesInHorizon).
2026-02-20 08:46:57 +02:00
Olaoluwa Osuntokun
14a01eb84f
Merge pull request #10577 from ellemouton/elle-cheaper-dedupe-model
ci: use haiku model for issue dedupe workflow
2026-02-17 17:28:19 -08:00
Olaoluwa Osuntokun
ba4778321d
Merge pull request #10576 from bhandras/bh/ephemeral-gocache-check-each-commit
scripts: use ephemeral gocache in check-each-commit
2026-02-17 17:28:06 -08:00
Olaoluwa Osuntokun
c11c192b3f
Merge pull request #10142 from Roasbeef/actor-mailbox-v2
actor: add new abstraction over mailbox
2026-02-17 17:27:45 -08:00
ziggieXXX
12b15c14a8
Merge pull request #10571 from ziggie1984/add-verify-release-workflow
ci: add verify-release workflow
2026-02-16 16:33:23 +01:00
Yong
f9e3825601
Merge pull request #10414 from lightningnetwork/elle-g175Prep-base
[g175] graph/db: merge g175 types-prep side branch
2026-02-14 00:22:59 +08:00
ziggie
99eceba7a5
scripts: switch verify-install.sh from curl to wget
Replace curl with wget for downloading release manifests and
signatures in verify-install.sh. wget handles redirects, retries, and
error reporting more robustly by default, which avoids silent download
failures that caused misleading "Invalid signature!" errors.

Also add error checking to all download calls so failures are reported
immediately with the URL that failed, and log which signature file and
user failed gpg verification.
2026-02-13 10:35:11 +01:00
ziggie
b7c4755934
scripts: log failing signature details in verify-install
When gpg --verify fails, include the signature filename, username,
and full GPG output in the error message. Previously only a generic
"Invalid signature!" was printed, making it hard to identify which
signer's signature was invalid.
2026-02-13 10:35:11 +01:00
ziggie
5e3d608761
ci: add verify-release workflow
Add a workflow that triggers when a release is published. It runs
verify-install.sh inside the official Docker image to validate
signatures and binary hashes. If verification fails, the release
is automatically set back to draft.
2026-02-13 10:35:11 +01:00
Elle Mouton
5f429f0aff
ci: use haiku model for issue dedupe workflow
Switch the claude-dedupe-issues workflow from the default (most
expensive) model to claude-haiku-4-5, which is significantly cheaper
and sufficient for issue duplicate detection.
2026-02-13 10:52:46 +02:00
Andras Banki-Horvath
75de3e3b68
scripts: use ephemeral gocache in check-each-commit 2026-02-13 09:42:24 +01:00
Elle Mouton
4d4b42d0ea
docs: add release notes for gossip v2 policy support 2026-02-13 07:29:40 +02:00
Elle Mouton
1e35f258e4
graph/db: add gossip version parameter to ForEachNodeChannel
Update ForEachNodeChannel to accept a gossip version parameter,
allowing callers to specify which gossip version's channels should be
iterated. This change mirrors the approach taken in ForEachChannel and
prepares the graph database for supporting multiple gossip versions
while maintaining backward compatibility.

The Store interface is updated to include the version parameter, and
both KVStore and SQLStore implementations are updated accordingly:

- KVStore validates that only GossipVersion1 is requested, returning
  ErrVersionNotSupportedForKVDB for other versions.
- SQLStore passes the version through to the underlying node query,
  enabling version-specific channel iteration.

The ChannelGraph wrapper is updated to accept and pass through the
version parameter. VersionedGraph gains a ForEachNodeChannel method
that automatically uses its configured gossip version, providing a
clean interface for version-specific operations.

Update all call sites to explicitly pass lnwire.GossipVersion1, except
for the local channel manager in server.go which now uses the v1Graph
directly (matching the pattern used in other parts of the codebase).
2026-02-13 07:21:24 +02:00
Elle Mouton
d948c5bfad
graph/db: convert testEdgePolicyCRUD to versioned test
Convert testEdgePolicyCRUD to a versioned test that runs against both
v1 and v2 gossip versions. Update the test to use version-specific
edge creation helpers and to test version-specific fields and flag
behavior (ChannelFlags/MessageFlags for v1, DisableFlags/
ExtraSignedFields for v2).
2026-02-13 07:21:24 +02:00
Elle Mouton
677e6ea89c
sqldb: use version-specific staleness checks in UpsertChannelPolicy
Update the UpsertChannelPolicy query to apply different staleness
checks based on gossip version. For v1 policies, continue checking
last_update timestamps. For v2 policies, check block_height instead,
using >= comparison to handle policies from the same block.

The version-specific WHERE clause ensures that policy updates are only
applied when they contain newer information according to the versioning
scheme appropriate for that gossip version.
2026-02-13 07:21:24 +02:00
Elle Mouton
0cfb9c9d2c
graph/db: add gossip version parameter to ForEachChannel
Update ForEachChannel to accept a gossip version parameter, allowing
callers to specify which gossip version's channels should be iterated.
This change prepares the graph database for supporting multiple gossip
versions while maintaining backward compatibility.

The KVStore implementation validates that only GossipVersion1 is
requested, returning ErrVersionNotSupportedForKVDB for other versions.
The SQLStore implementation validates known versions and passes the
version through to the underlying paginated query.

Update VersionedGraph to include a ForEachChannel method that
automatically uses its configured gossip version, and update the
DescribeGraph RPC handler to use the v1Graph instead of the global
graphDB.
2026-02-13 07:21:24 +02:00
Elle Mouton
506ec037e2
graph/db: add versioned HasChannelEdge method
Add a new HasChannelEdge method that takes a gossip version parameter
and returns only existence and zombie status, without timestamp data.
This supports both v1 and v2 gossip protocols.

The original HasChannelEdge method is renamed to HasV1ChannelEdge to
preserve v1-specific functionality for callers that need timestamp
information. All call sites are updated accordingly.

The SQL store implementation now handles both gossip versions, using
timestamps for v1 and block heights for v2 policies, with proper
reject cache support for both versions.
2026-02-13 07:21:24 +02:00
Elle Mouton
4a6213c35e
graph/db: version reject cache for policies
Make the reject cache version-aware so v1 and v2 policy state can be
cached independently per channel ID.  Add helpers to store v1 timestamps
or v2 block heights and thread the versioned cache key through KV/SQL
store cache accesses.
2026-02-13 07:21:24 +02:00
Elle Mouton
40e3ac964f
graph/db: update policy builders to support v2 fields
Update buildChanPolicy and related functions in both KV and SQL stores
to properly construct ChannelEdgePolicy with version-specific fields:

KVStore changes:
- Reject non-v1 policies in updateEdgePolicy and serializeChanEdgePolicy
  since KV store only supports v1 gossip protocol.
- Set Version to GossipVersion1 when deserializing policies from KV.

SQLStore changes:
- Add isNode1 parameter to buildChanPolicy functions to properly set
  SecondPeer field (v2 uses SecondPeer instead of ChannelFlags direction).
- Extract Version from database and populate version-specific fields:
  - For v1: MessageFlags, ChannelFlags, LastUpdate, ExtraOpaqueData
  - For v2: DisableFlags, LastBlockHeight, ExtraSignedFields
- Thread isNode1 through buildChanPolicyWithBatchData and
  buildCachedChanPolicies call sites.

This enables the SQL store to read and reconstruct both v1 and v2
channel policies from the database with proper field mapping.
2026-02-13 07:21:24 +02:00
Elle Mouton
bd215b1a7d
graph/db: set policy version for cache load
Ensure policies loaded for graph cache reconstruction are tagged as v1.
2026-02-13 07:21:24 +02:00
Elle Mouton
a73c45946b
multi: add ChanEdgePolicyFromWire constructor for ChannelEdgePolicy
Add ChanEdgePolicyFromWire to construct ChannelEdgePolicy from channel
update messages, centralizing v1/v2 field mapping.
Update call sites to use the helper:

- discovery/gossiper: handleChanUpdate
- graph/builder: ApplyChannelUpdate
- routing/router_test: ApplyChannelUpdate test helper

This consolidates update-to-policy conversion logic across versions.
2026-02-13 07:21:24 +02:00
Elle Mouton
9c7a12c0e0
graph/db: refactor CachedEdgePolicy to use explicit boolean fields
Replace MessageFlags and ChannelFlags bitfields in CachedEdgePolicy
with explicit boolean fields to improve clarity and support both v1
and v2 channel updates:

- Replace MessageFlags with HasMaxHTLC boolean.
- Replace ChannelFlags with IsNode1 and IsDisabled booleans.
- Update NewCachedPolicy to extract these fields version-appropriately:
  - For v1: derive from MessageFlags and ChannelFlags bits.
  - For v2: derive from policy.SecondPeer and policy.DisableFlags.

Update all call sites that used method calls IsNode1() and IsDisabled()
to instead access the fields directly. This includes:
- graph_cache.go: policy direction and disable checks
- unified_edges.go: HasMaxHTLC and IsDisabled checks
- Tests: policy construction and assertions

This refactoring improves readability by making the cached policy's
state explicit rather than encoded in bitfields, and enables seamless
support for both gossip protocol versions.
2026-02-13 07:21:24 +02:00
Elle Mouton
3fe40c6540
graph/db: add v2 fields to ChannelEdgePolicy model
Extend ChannelEdgePolicy to support v2 channel updates by adding:

- Version field to track gossip protocol version (v1 or v2).
- LastBlockHeight for v2's block-height-based timestamps.
- SecondPeer flag to indicate which peer announced the policy in v2.
- DisableFlags for v2-specific channel disable signaling.
- ExtraSignedFields map for v2 extra signed TLV data.

Add version-aware methods:
- IsNode1() determines if the policy was announced by node_1, handling
  both v1 (via ChannelFlags direction bit) and v2 (via SecondPeer).
- IsDisabled() checks disable status using ChannelFlags for v1 and
  DisableFlags for v2.
- String() provides version-appropriate string representations.

The new fields use zero values for v1 compatibility (Version defaults
to GossipVersion1, LastBlockHeight to 0, SecondPeer to false). This
lays the groundwork for v2 policy support; a subsequent commit will
handle reading and writing these fields from/to the database.
2026-02-13 07:21:24 +02:00
Elle Mouton
0aa93c93e7
graph/db: add v2 policy fields to database layer
Extend channel policy queries and structs to support v2-specific fields:

- Add BlockHeight field to track the block height for v2 policy updates.
- Add DisableFlags field for v2 channel disable messages.

Both fields are nullable (sql.NullInt64/Int16) to maintain backwards
compatibility with v1 channels. The fields are initialized as null in
updateChanEdgePolicy and threaded through all policy-related queries
(GetChannelBySCIDWithPolicies, ListChannelsByNodeID, UpsertEdgePolicy,
etc.) and the extractChannelPolicies helper.

This commit includes both the hand-written SQL query updates and the
corresponding sqlc-generated Go code.
2026-02-13 07:21:23 +02:00
Elle Mouton
6328c4d897
graph/db: treat empty channel signatures as missing
This commit improves handling of missing channel signatures in the
database:

- Return nil from auth proof accessors instead of empty slices so that
  missing signatures are stored as NULL in SQL.
- Update public channel checks to require signature length > 0, which
  properly handles existing empty bytea values in the database.
- Add regression test covering empty v1 and v2 channel signatures to
  prevent future issues.
2026-02-13 07:21:23 +02:00
Elle Mouton
a39f94eb4a
docs: update release notes for gossip v2 graph preparation
Add reference to the PR implementing v2 channel support in the graph
database layer.
2026-02-13 07:21:23 +02:00
Elle Mouton
03050a589d
graph/db: add version parameter to IsZombieEdge
Make IsZombieEdge version-aware and add corresponding method to
VersionedGraph. Convert TestEdgeInsertionDeletion to versioned test
using the createEdge helper for both v1 and v2 channel testing.
2026-02-13 07:21:23 +02:00
Elle Mouton
d88fa01162
graph/db: add version parameter to FetchChannelEdgesByID/Outpoint
Make channel edge fetching version-aware by adding gossip version
parameter to FetchChannelEdgesByID and FetchChannelEdgesByOutpoint.
Add corresponding methods to VersionedGraph. V2 policy building is
marked as TODO.
2026-02-13 07:21:23 +02:00
Elle Mouton
971c0f6eb8
graph/db: add version parameter to IsPublicNode
Make IsPublicNode version-aware by routing to the appropriate SQL
query based on gossip version. V1 and v2 have different criteria for
determining node publicity (v1 requires four signatures, v2 requires
one). Convert TestNodeIsPublic to versioned test for both protocols.
2026-02-13 07:21:23 +02:00
Elle Mouton
a99604c7d7
sqldb/sqlc: add IsPublicV2Node query
Add SQL query to determine if a node has public v2 channels. Unlike
v1 which requires all four individual signatures, v2 channels are
considered public when the single aggregated signature is present.
2026-02-13 07:21:23 +02:00
Elle Mouton
b3cf9a806f
multi: add DeleteChannelEdges to VersionedGraph
Add version-aware DeleteChannelEdges method to VersionedGraph and
update call sites in graph builder, rpcserver, and server to use
versioned graphs. This ensures channel deletion operations are
properly scoped to the correct gossip version.
2026-02-13 07:21:23 +02:00
Elle Mouton
dcd0b9dac7
graph/db: make TestPartialNode version-aware
Convert TestPartialNode to a versioned test that runs for both v1 and
v2 gossip versions, ensuring shell node creation works correctly for
both channel types.
2026-02-13 07:21:23 +02:00
Elle Mouton
99d622ca03
graph/db: support v2 channel proofs in AddEdgeProof
Update AddEdgeProof to handle both v1 (four separate signatures) and
v2 (single aggregated signature) channel authentication proofs using
the appropriate SQL queries.
2026-02-13 07:21:23 +02:00
Elle Mouton
315f5ed741
sqldb/sqlc: add AddV2ChannelProof query
Add SQL query to update the signature column for v2 channel auth proofs.
Unlike v1 which requires four separate signatures, v2 channels use a
single aggregated signature.
2026-02-13 07:21:23 +02:00
Elle Mouton
a73189ccec
graph/db: add version parameter to DeleteChannelEdges
Thread the gossip version through DeleteChannelEdges to enable
version-aware channel deletion. The KVStore rejects non-v1 versions
while SQLStore properly passes the version to the underlying queries.
This prepares for v2 channel zombie handling (strict zombie pruning
for v2 is marked as TODO).
2026-02-13 07:21:22 +02:00
Elle Mouton
6bb3bdc83d
graph/db: add version parameter to createEdge test helper
Extend the createEdge test helper to accept a gossip version, enabling
creation of both v1 and v2 test channels. V2 channels include the
appropriate auth proof (single signature), merkle root hash, and
funding script fields.
2026-02-13 07:21:22 +02:00
Elle Mouton
86732fae99
graph/db: refactor TestNodeIsPublic to use require assertions
Replace verbose t.Fatal patterns with concise require assertions
following the project's testing guidelines.
2026-02-13 07:21:22 +02:00
Elle Mouton
fa24d336c1
graph/db: update SQLStore to read and write v2 channels
Extends the SQL store to support v2 (taproot) channel announcements:
- Add version validation in AddChannelEdge
- Store v2-specific fields: FundingPkScript, MerkleRootHash, Signature
- Update buildEdgeInfoWithBatchData to reconstruct v2 channels from DB
  with optional bitcoin keys and funding script handling
- Add WithMerkleRootHash edge modifier for ChannelV2Fields
2026-02-13 07:21:22 +02:00
Elle Mouton
6842252bf8
graph/db: add v2 channel announcement fields to ChannelEdgeInfo
This commit extends ChannelEdgeInfo to support v2 (taproot) channel
announcements by adding:
- MerkleRootHash for the optional Merkle tree commitment
- ExtraSignedFields for additional signed TLV fields
- ChannelV2Fields struct to encapsulate v2-specific optional fields
- NewV2Channel constructor for creating v2 channel edges
- FundingPKScript handling for MuSig2 key aggregation with optional
  taproot tweaks
2026-02-13 07:21:22 +02:00
Elle Mouton
25bbb39030
docs: update release notes
Update the Database section to reference both PRs that prepare the
graph DB for gossip v2 support:
- PR 10339: node handling
- PR 10379: channel handling (this PR)
2026-02-13 07:21:22 +02:00
Elle Mouton
06a669bc89
graph/db: add FundingPKScript method on ChannelEdgeInfo
Add FundingPKScript() method that returns the funding output's
pkScript for the channel. The implementation is version-aware:

- V1: generates a 2-of-2 multisig P2WSH script from the two bitcoin
  keys
- V2: will use taproot script (to be implemented)

This encapsulates the script generation logic and makes it clear
which bitcoin keys are being used. Replaces direct calls to
genMultiSigP2WSH with the cleaner method call.
2026-02-13 07:21:22 +02:00
Elle Mouton
44f4171415
graph/db: make some ChannelEdgeInfo fields optional
Since not all will be required for V2 channels.

Wrap BitcoinKey1Bytes and BitcoinKey2Bytes in fn.Option since these
fields are only required for v1 channel announcements. V2 channels may
or may not have bitcoin keys present in their announcement.

NewV1Channel constructor wraps the bitcoin keys with fn.Some().
All access sites updated to unwrap the options, using UnwrapOr for
non-critical paths and UnwrapOrErr where the keys must be present
(e.g., KV serialization, ToChannelAnnouncement).
2026-02-13 07:21:22 +02:00
Elle Mouton
830b9db69c
multi: add ToChannelAnnouncement helper on ChannelEdgeInfo
So that we have one place that converts from our `models` struct to the
`lnwire.ChannelAnnouncement` struct.

The commit also refactors netann.CreateChanAnnouncement to only take a
ChannelEdgeInfo and get the proof from there instead of needing the
proof to be passed in separately.

Add ToChannelAnnouncement() method to ChannelEdgeInfo that converts
the model struct to a lnwire.ChannelAnnouncement1 message. This:

- Centralizes the conversion logic in one place instead of scattered
  across multiple call sites
- Validates that AuthProof is present (can't create announcement
  without proof)
- Currently only supports v1 channels, returning error for v2

Refactor netann.CreateChanAnnouncement to use this helper and remove
the separate chanProof parameter since proof is now accessed from
within ChannelEdgeInfo. This improves encapsulation and reduces
parameter count.
2026-02-13 07:21:22 +02:00
Elle Mouton
ba6222b521
multi: add and use V1 constructor for models.ChannelEdgeInfo
This makes it clear what fields must/can be set for a V1 channel.

Introduce NewV1Channel constructor to create v1 channel edges with
proper initialization and validation. The constructor:

- Takes required fields (chanID, chainHash, node keys) as parameters
- Takes v1-specific fields (bitcoin keys, extra opaque data) via
  ChannelV1Fields struct
- Accepts optional fields (capacity, channel point, features, proof)
  via functional options (WithCapacity, WithChannelPoint, etc.)
- Validates that if an AuthProof is provided, its version matches the
  channel version

This makes it clear which fields are required vs optional for v1
channels and prevents incorrectly initialized channel edges. All
tests and production code updated to use the constructor.
2026-02-13 07:21:22 +02:00
Elle Mouton
7a260ee103
multi: update models.ChannelAuthProof with v2 field
Also update it to more closely match the persisted version which has the
v1 and v2 only fields as optional.

Refactor ChannelAuthProof to support both v1 and v2 channel
announcements:

- Add Version field to distinguish v1 from v2 proofs
- Wrap v1-specific fields (NodeSig1/2, BitcoinSig1/2) in fn.Option
  since v2 doesn't use them
- Add optional Signature field for v2's single schnorr signature
- Add constructor functions NewV1ChannelAuthProof and
  NewV2ChannelAuthProof to enforce correct initialization
- Add getter methods (NodeSig1(), BitcoinSig1(), etc.) that safely
  unwrap options, returning empty slices when not present

The IsEmpty() check is updated to handle both versions correctly.
Both stores validate v1-only for now.
2026-02-13 07:21:22 +02:00
Elle Mouton
4480352d59
multi: add version to models.ChannelEdgeInfo
And set it to V1 version everywhere.

Add a Version field to ChannelEdgeInfo to distinguish between v1 and
v2 channel announcements. Set it to GossipVersion1 for all existing
channels.

Both KV and SQL stores now validate that only v1 channels are
currently supported, returning an error for v2 channels. The KV store
automatically sets version to v1 when deserializing (since all
persisted channels in KV format are v1).

This versioning is essential for handling the different field
requirements and validation logic between v1 and v2 channels.
2026-02-13 07:21:21 +02:00
Elle Mouton
ec46480a86
sqldb/sqlc: update graph CreateChannel query for v2
Add three new optional fields to the CreateChannel SQL query to
support v2 channel announcements:
- signature: single schnorr signature (replaces four ECDSA sigs)
- funding_pk_script: the funding output script
- merkle_root_hash: for taproot channels

These fields are NULL for v1 channels and populated for v2 channels.
2026-02-13 07:21:21 +02:00
Elle Mouton
b13b0bb922
graph/db: expose version in maybeCreateShellNode
Add a version parameter to maybeCreateShellNode to allow callers to
specify which gossip protocol version should be used when creating
shell nodes. Currently all callers pass GossipVersion1, but this
change sets up the foundation for v2 support.

Shell nodes are lightweight node entries containing only a protocol
version and public key, created before full node information is
available.
2026-02-13 07:21:21 +02:00
Elle Mouton
34f74c71ce
graph/db: use route.Vertex for channel keys
Replace [33]byte with route.Vertex for NodeKey1Bytes, NodeKey2Bytes,
BitcoinKey1Bytes, and BitcoinKey2Bytes in ChannelEdgeInfo. Since
route.Vertex is defined as [33]byte, this change is functionally
equivalent but provides better type safety and consistency with the
rest of the routing subsystem.

OtherNodeKeyBytes is also updated to return route.Vertex.
2026-02-13 07:21:21 +02:00
Elle Mouton
6fda2e8da9
graph/db: remove unused sig field from ChannelEdgePolicy
Remove the cached parsed signature field and its lazy getter method
from ChannelEdgePolicy. This field was unused throughout the codebase
and the signature is already stored as raw bytes in SigBytes.

The SetSigBytes method is updated to remove the cache invalidation
logic.
2026-02-13 07:21:21 +02:00
Elle Mouton
fcbfeb33e0
docs: update release notes 2026-02-13 07:21:21 +02:00
Elle Mouton
7bb38651d9
graph/db: convert other node CRUD methods
Convert a few more CRUD methods to handle v2 nodes. Then update some
more tests to be run against both v1 and v2 nodes.
2026-02-13 07:21:21 +02:00
Elle Mouton
7a5a588176
graph/db: add TestVersionedDBs
This will be used to run sub-tests with v2 data.
2026-02-13 07:21:21 +02:00
Elle Mouton
d2b47cdbae
graph/db: let node helpers take version 2026-02-13 07:21:21 +02:00
Elle Mouton
4db5198079
graph/db: introduce VersionedGraph
For now, all instances will use V1 only so as not to change behaviour
yet.
2026-02-13 07:21:21 +02:00
Elle Mouton
d8337be968
graph/db: add helper variable for tests
Add an isSQLDB variable that will let us quickly check in tests if the
backing DB is KVStore or SQLStore.
2026-02-13 07:21:21 +02:00
Elle Mouton
c281810ab9
graph/db: move gossip versions up one layer
Update some of the node related graph CRUD methods to take a version
rather than hardcoding them in the SQLStore layer. Move the version up
one layer instead. This will make it easier to make it configurable
later on.
2026-02-13 07:21:20 +02:00
Elle Mouton
97143a74ab
graph/db: split HasNode into two methods
HasNode currently is very v1 specific since it returns a time.Time
timestamp which is specific to V1 node announcements. However, it is
mostly only used for the "exists" return value. So here we split it up
into HasNode which just checks existence and HasV1Node which retains the
same behavaiour as before.
2026-02-13 07:21:20 +02:00
Elle Mouton
5a3c013e91
sqldb/sqlc: add NodeExists query 2026-02-13 07:21:20 +02:00
Elle Mouton
1738e0cabc
graph/db: allow v2 nodes
Update the SQLStore node writer and readers to handle V2 ndoes.
Currently no logic will actually add such nodes. The following commits
will update what is needed so that CRUD for v2 nodes can be tested.
2026-02-13 07:21:20 +02:00
Elle Mouton
628b89cc40
graph/db: explicit redirect to Store
Instead of embedding Store in ChannelGraph so that any methods of the
Store interface not implemented by the ChannelGraph are redirected to
the underlying Store, we update things in this commit to instead
make the "redirection" explicit. This is in preparation for changes we
will make soon where some underlying store methods will take an explicit
"version" parameter but then we will keep the ChannelGraph methods as is
so that existing call-sites dont all need to be updated. We will then
add "Versioned" ChannelGraph wrapper which decides the version use.
Initially, most call-sites will just create a wrapped V1 ChannelGraph so
that the logic remains as it is today.
2026-02-13 07:21:20 +02:00
Elle Mouton
c61617200b
sqldb: update node query for v2
Here we update the UpdateNode query so that it can be used to insert
the new blockheight field for a v2 node.
2026-02-13 07:21:20 +02:00
Elle Mouton
3dfc835354
graph/db: add V2 Node constructor
We add a new NewV2Node constructor which takes a new NodeV2Fields as a
parameter. This NodeV2Fields struct defines the fields that can be set
in a models.Node if the version is V2.
2026-02-13 07:21:20 +02:00
Elle Mouton
bc76383e15
graph/db: rename V1Store to Store
The underling store will store gossip messages across gossip versions
and we will instead expose version parameters on many of the methods. So
this interface really just abstracts the underlying store/schema type.
2026-02-13 07:21:20 +02:00
Elle Mouton
cb2fc6621e
sqldb: add new gossip v2 columns to graph tables
Add a new migration that updates the graph tables (nodes, channels and
policies) in preparation for the new columns required for V2
announcements. This migration has to be added to the set of "live"
migrations instead of "dev only" since it edits the columns of existing
tables and so changes the existing sql models. We are going to prep the
SQLStore code to handle the V2 types in the coming commits, so we need
this migration to be in place.

In this commit we also remove the TestSchemaMigrationIdempotency test
since this test fails with the new "ALTER TABLE" migrations which dont
have "IF NOT EXISTS" options like tables and indexes do. Migrations
should be idempotent anyways due to the migration tracker file and/or
the sqlc migration tracker.
2026-02-13 07:21:20 +02:00
Elle Mouton
2e58c07570
devrpc: fix comment 2026-02-13 07:21:19 +02:00
ziggieXXX
b1165e984e
Merge pull request #10464 from gijswijs/fix-itest-flakes
Fix flaky watchtower and invoice tests
2026-02-12 15:30:46 +01:00
Yong
1bdd0f9a29
Merge pull request #10562 from ziggie1984/fix-severity-cmd-classification
build: classify cmd/* as MEDIUM in PR severity bot
2026-02-12 21:03:24 +08:00
Gijs van Dam
09379d0381 wtclient: fix race conditions locking tests
This commit fixes two flaky test scenarios:

1. testRemoveLockedAddr: Add synchronization to wait for the dial to
   start before asserting that the address is locked. Previously, the
   test could race and check the lock state before session negotiation
   began.

2. testTowerSwitch: Use wait.Predicate for RemoveTower since the
   address may still be locked by an active session, causing
   intermittent failures.
2026-02-12 13:02:02 +01:00
Gijs van Dam
6ea99cde42 invoices: increase timeout parallel Postgres tests
Increase the test timeout from 10s to 60s to accommodate slow
Postgres database setup and migrations when running tests in
parallel. This prevents false-positive test failures on slower
CI runners.
2026-02-12 13:01:50 +01:00
Olaoluwa Osuntokun
b3e37edfae
Merge pull request #10563 from Roasbeef/go-1-25
build+docs: bump minimum Go version to 1.25.5
2026-02-11 14:55:30 -08:00
ziggieXXX
3267fec34a
Merge pull request #10536 from starius/unlock-wait
lncli unlock: wait until daemon can unlock
2026-02-11 17:28:55 +01:00
ziggieXXX
17b956d320
Merge pull request #10559 from lightningnetwork/elle-issue-dedupe
.claude+.github: add issue dedupe workflow
2026-02-11 14:29:04 +01:00
ziggie
325f83c179
sqldb+lncfg: consolidate SQLite default constants
Move the SQLite default constants (max connections, busy timeout) to
sqldb/config.go as the single source of truth and export them. Remove
the duplicate definitions from lncfg/db.go and reference the sqldb
constants instead.
2026-02-11 13:15:56 +01:00
ziggie
d87d93b114
sqldb+lncfg: use SQLite-appropriate default for max connections
Both the sqldb and kvdb SQLite layers were not using a sensible default
for MaxConnections. The sqldb store used defaultMaxConns (25) which is
meant for Postgres, and the kvdb path passed 0 (unlimited) to
sqlbase.Init when unconfigured.

Add a MaxConns() method on SqliteConfig that returns the configured
value or defaults to 2, appropriate for SQLite's single-writer model.
Use it in both sqldb/sqlite.go and lncfg/db.go so both layers share
the same default.
2026-02-11 13:15:56 +01:00
ziggie
a7d1a30cf2
sqldb: wire up PragmaOptions config for SQLite store
The SqliteConfig.PragmaOptions field existed but was never appended to
the DSN. Add the loop to apply user-specified pragma options after the
built-in ones, matching the existing behavior in kvdb/sqlite.
2026-02-11 13:15:56 +01:00
ziggie
57fd965932
sqldb: wire up BusyTimeout config for SQLite store
The SqliteConfig.BusyTimeout field existed but was never used — the
busy_timeout pragma was hardcoded to 5000ms. Add a busyTimeoutMs()
helper that returns the configured value or falls back to the 5000ms
default, and use it when constructing the SQLite DSN.
2026-02-11 13:15:56 +01:00
Elle Mouton
7baead7183
multi: add issue dedupe command and workflow
- add /dedupe command definition for Claude Code
- add workflow to run dedupe on issue opened
- add comment helper script for posting duplicates
2026-02-11 13:06:42 +02:00
Yong
e53c4b1de5
Merge pull request #10465 from ziggie1984/bugfix/fix-peer-disconnect-log
peer: fix log output when not applicable
2026-02-11 11:41:20 +08:00
Olaoluwa Osuntokun
ba1fb50380 build+docs: bump minimum Go version to 1.25.5
With Go 1.26 now released, this bumps the minimum required Go version
from 1.24.11 to 1.25.5 across all go.mod files and updates the
installation documentation with the correct download links and SHA256
hashes for Go 1.25.5 binaries.

The build system (Dockerfiles, Makefile, CI) was already using Go 1.25.5,
so this change aligns the go.mod minimum version to match.
2026-02-10 12:54:10 -08:00
Olaoluwa Osuntokun
83163a0fe7
Merge pull request #10553 from lightningnetwork/claude-chill
ci: reduce pr-severity bot comment spam
2026-02-10 09:54:25 -08:00
ziggie
ad8b64417f
build: classify cmd/* as MEDIUM in PR severity bot
The severity bot was misclassifying CLI client code as HIGH because
filenames like cmd_walletunlocker.go matched the walletunlocker/*
auth/security keyword. Add cmd/* explicitly to the MEDIUM tier and
add a classification rule to prevent filename-based false positives.
2026-02-10 10:48:09 +01:00
Olaoluwa Osuntokun
20c5c9d6be actor: refactor Actor to use Mailbox interface
This commit refactors the Actor implementation to use the new Mailbox
interface instead of directly managing a channel. This change
significantly simplifies the actor's message processing loop and
improves separation of concerns.

The main changes include replacing the direct channel field with a
Mailbox interface, updating NewActor to create a ChannelMailbox
instance, and refactoring the process method to use the iterator
pattern provided by mailbox.Receive. The new implementation uses a
clean for-range loop over the mailbox's message iterator, eliminating
the complex select statement that previously handled both message
reception and context cancellation.

The Tell and Ask methods in actorRefImpl have been simplified to use
the mailbox's Send method, which internally handles both the caller's
context and the actor's context. This eliminates the need for complex
select statements in these methods and ensures consistent context
handling throughout the actor system.

Message draining during shutdown is now handled through the mailbox's
Drain method, providing a cleaner separation between normal message
processing and cleanup operations. The actor still properly sends
unprocessed messages to the Dead Letter Office and completes pending
promises with appropriate errors during shutdown.
2026-02-09 19:39:06 -08:00
Olaoluwa Osuntokun
de7b0e1dd4 actor: add tests for mailbox implementation
This commit adds thorough test coverage for the new Mailbox interface
and ChannelMailbox implementation. The tests verify correct behavior
across various scenarios including successful sends, context
cancellation, mailbox closure, and concurrent operations.

The test suite specifically validates that the mailbox respects both
the caller's context and the actor's context during send and receive
operations. This ensures that actors properly shut down when their
context is cancelled, and that callers can cancel operations without
affecting the actor's lifecycle.

Additional tests cover edge cases such as zero-capacity mailboxes
(which default to a capacity of 1), draining messages after closure,
and concurrent sends from multiple goroutines. The concurrent test
uses 10 senders each sending 100 messages to verify thread-safety
and proper message ordering.

All tests pass with the race detector enabled, confirming the
implementation is free from data races.
2026-02-09 17:03:24 -08:00
Olaoluwa Osuntokun
30af28104b actor: introduce generic Mailbox interface with iter.Seq support
This commit introduces a new Mailbox interface that abstracts the
message queue implementation for actors. Previously, actors used a
direct channel for their mailbox, which limited flexibility and made
it difficult to implement alternative mailbox strategies.

The new Mailbox interface provides methods for sending, receiving, and
draining messages, with full context support for cancellation. The
Receive method leverages Go 1.23's iter.Seq pattern, providing a clean
iterator-based API that allows natural for-range loops over messages.

The ChannelMailbox implementation maintains the existing channel-based
behavior while conforming to the new interface. It stores the actor's
context internally, ensuring both caller and actor contexts are
properly respected during send and receive operations. This simplifies
context handling compared to complex context merging approaches.

This abstraction enables future implementations such as priority
mailboxes, persistent mailboxes, or bounded mailboxes with overflow
strategies, without requiring changes to the actor implementation.
2026-02-09 17:03:24 -08:00
Olaoluwa Osuntokun
3807c6c563
Merge pull request #9820 from lightningnetwork/actor
actor: add new package for structured concurrency based on the Actor model
2026-02-09 17:02:02 -08:00
Olaoluwa Osuntokun
8781daa995
actor: add README.md
In this commit, we add a readme which serves as a general introduction
to the pacakge, and also the motivation of the package. It serves as a
manual for developers that may wish to interact with the package.
2026-02-06 19:26:54 -08:00
Olaoluwa Osuntokun
7c9d72134c
actor: add example files
In this commit, we add a series of examples that show how the package
can be used in the wild. They can be run as normal Example tests.
2026-02-06 19:26:54 -08:00
Olaoluwa Osuntokun
9ebd8d240d
actor: add the actor system and router
In this commit, we add the actor system (along with the receiptionist)
and the router.

An actor can be registered with the system, which allows other callers
to locate it to send message to it via the receptionist. Custom routers
can be created for when there're actors that rely on the same service
key and also req+resp type. This can be used to implement something
similar to a worker pool.
2026-02-06 19:26:54 -08:00
Olaoluwa Osuntokun
b055af6420
actor: add fundamental interfaces and concrete Actor impl
In this commit, we add the actual Actor implementation. We define a
series of types and interfaces, that in concert, describe our actor. An
actor has some ID, a reference (used to send messages to it), and also a
set of defined messages that it'll accept.

An actor can be implemented using a simple function if it's stateless.
Otherwise, a struct can implement the Receive method, and handle its
internal message passing and state that way.
2026-02-06 19:26:54 -08:00
Olaoluwa Osuntokun
3069829d61
actor: add Future[T] and Promise[T] w/ concrete impls
In this commit, we add two new fundamental data structures: Future[T]
and Promise[T].

A future is a response that might be ready at some point in the future.
This is already a common pattern in Go, we just make a type safe wrapper
around the typical operations: block w/ a timeout, add a call back for
execution, pipeline the response to a new future.

A promise is an intent to complete a future. Typically the caller
receives the future, and the callee is able to complete the future using
a promise.
2026-02-06 19:26:54 -08:00
Olaoluwa Osuntokun
90415aaa04
actor: add new actor package as distinct sub-module 2026-02-06 19:26:53 -08:00
ziggieXXX
14b90a27cf
Merge pull request #10507 from hieblmi/wallet-tip
rpcserver: add `wallet_synced` to `GetInfoResponse`
2026-02-05 17:55:02 -05:00
Leo
41eaf95897
remove link to etcd guide from install.md 2026-02-05 13:56:03 -08:00
Leo
356a8ae0c5 Revert "remove link to etcd guide from install.md"
This reverts commit 77a6768a07.
2026-02-05 13:55:38 -08:00
Leo
77a6768a07 remove link to etcd guide from install.md 2026-02-05 13:54:27 -08:00
Leo
6147bee49c
remove redundant documents from /docs 2026-02-05 12:34:51 -08:00
Slyghtning
7766c1dd9a
docs: add release notes for wallet_synced RPC field 2026-02-05 08:58:33 +01:00
Slyghtning
b8693bea33
itest: add wallet_synced field integration test
In this commit, we add an integration test that verifies the
wallet_synced field in GetInfoResponse correctly reflects the wallet's
sync state.

The test creates a node, verifies wallet_synced becomes true after
initial sync, then stops the node and mines blocks while it's offline.
After restart, the test polls GetInfo to observe the wallet catching up,
ideally capturing the transition from wallet_synced=false to true.

The test is registered in the "wallet sync" test case group.
2026-02-05 08:58:33 +01:00
Slyghtning
d39577682e
rpcserver: populate wallet_synced in GetInfo response
In this commit, we extend the chainSyncInfo struct with a new
isWalletSynced field that tracks the wallet's sync state independently
from the composite isSynced field. The GetInfo RPC handler now populates
the WalletSynced response field from this new struct field.

A debug log line is added to GetInfo to help diagnose sync state issues,
showing both the composite sync status and the wallet-specific sync
status.

Currently isWalletSynced mirrors isSynced since both ultimately derive
from the same underlying wallet sync check. This prepares the plumbing
for future differentiation where wallet sync state could be tracked
separately from router and blockbeat dispatcher states.
2026-02-05 08:58:31 +01:00
Slyghtning
3c22c3e7fa
lnrpc: add wallet_synced field to GetInfoResponse
In this commit, we add a new `wallet_synced` boolean field to the
GetInfoResponse message. This field exposes the wallet's internal sync
state with the backing chain source, providing visibility into whether
the wallet has caught up to the current chain tip.

This is distinct from the existing `synced_to_chain` field, which
represents a composite sync state that also considers the router and
blockbeat dispatcher. The new field allows callers to distinguish
between wallet sync delays and other subsystem sync states.
2026-02-05 08:45:53 +01:00
Olaoluwa Osuntokun
44c6cc5a1c
Merge pull request #10530 from NishantBansal2003/gossip-race
discovery: fix race on remoteUpdateHorizon in GossipSyncer
2026-02-04 17:48:58 -08:00
Boris Nagaev
d8f39fa1b4
docs/release-notes: add release notes entry 2026-02-04 15:21:03 -05:00
Boris Nagaev
593e8c01cf
docs/release-notes: fix PR reference 2026-02-04 15:20:03 -05:00
Boris Nagaev
6cd785c751
lncli unlock: add unit tests
Add table-driven unit tests for unlock() that exercise success and error paths,
cover flag and arg handling.
2026-02-04 15:20:03 -05:00
Boris Nagaev
e865552cc1
lncli unlock: wait until daemon can unlock
Use the StateService stream to wait for LOCKED before sending the unlock
request, then wait for UNLOCKED/RPC_ACTIVE before reporting success.
If the state shows the wallet is already unlocked, skip sending the
unlock request and return an error immediately.

This avoids lost unlocks during slow startup.
Fix https://github.com/lightningnetwork/lnd/issues/7749
2026-02-04 15:20:03 -05:00
Nishant Bansal
caa24791be
discovery: fix race on remoteUpdateHorizon in GossipSyncer
Guard access to remoteUpdateHorizon to prevent a race when
the gossiper is flushing a pending batch of announcements
while concurrently processing a GossipTimestampRange message
from a peer.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
2026-02-05 00:57:55 +05:30
Olaoluwa Osuntokun
5c7d3ad045
Merge pull request #10540 from morehouse/gossiper_shutdown_deadlock
discovery: fix gossiper shutdown deadlock
2026-02-04 10:32:15 -08:00
Olaoluwa Osuntokun
0871193cdf ci: reduce pr-severity bot comment spam
Only post a severity classification comment when the bot hasn't
commented before or when the severity actually changed. Previously
every push (synchronize event) would post a new comment even if the
classification was identical.

The prompt now instructs the classifier to:
- Check for existing bot comments via the pr-severity-bot marker
- Compare the new severity against the existing severity label
- Skip commenting if both match, while still ensuring labels are correct
- Include a severity changed banner when re-commenting due to a change
2026-02-04 10:02:53 -08:00
Yong
eaa7bcf67a
Merge pull request #10547 from gijswijs/fix-linter-for-worktrees
Fix linter in git worktrees
2026-02-04 20:26:06 +08:00
Olaoluwa Osuntokun
4d775457c5
Merge pull request #10550 from Roasbeef/fix-claude-again
ci: add tool constraints to pr-severity prompt
2026-02-03 21:45:46 -04:00
Yong
89b54abbc6
Merge pull request #10544 from ellemouton/updateBWCompatBaseVersion
scripts: update bw-compat test LND base version
2026-02-04 03:30:11 +08:00
Gijs van Dam
2643988524 make: fix linter in git worktrees
When running `make lint` in a git worktree, the diff processor fails
with "no version control repository found" because the Docker container
only mounts the worktree directory, not the main git directory that the
worktree's .git file references.

This causes golangci-lint's `new-from-rev` filter to not work, resulting
in all ~32k existing lint issues being reported instead of only newly
introduced ones.

Fix by detecting when we're in a worktree (.git is a file, not a
directory) and mounting the main .git directory into the container so
revgrep can access the git history.
2026-02-03 19:45:56 +01:00
Olaoluwa Osuntokun
7b93441814 ci: add tool constraints to pr-severity prompt
Claude keeps trying to use `gh api` to add severity labels, which gets
denied by the allowed tools restriction. Instead of retrying with the
permitted `gh pr edit --add-label` command, it silently gives up and
only posts the comment. The result is that severity comments appear on
PRs but the actual labels are never applied.

Add an explicit tool constraints section at the top of the prompt so
Claude knows upfront that only `gh pr view`, `gh pr edit`, and
`gh pr comment` are available.
2026-02-03 10:22:56 -08:00
Olaoluwa Osuntokun
d332cb0761
Merge pull request #10529 from Roasbeef/fix-edge-features-decode-compat
graphdb: fix backwards-compat for channel edge feature deserialization
2026-02-03 13:12:26 -04:00
Olaoluwa Osuntokun
56a7f45b99 graphdb: fix backwards-compat for channel edge feature deserialization
This commit fixes a backwards compatibility issue that prevented nodes
from upgrading from v0.19.x to v0.20.x.

In v0.19.x, channel edge features were serialized as raw feature bytes
without a length prefix. In v0.20.x (commit 2f2845dfc), the serialization
changed to use Features.Encode() which adds a 2-byte big-endian length
prefix before the feature bits. The deserialization code was updated to
use Features.Decode() which expects this length prefix.

When v0.20.x reads a database created by v0.19.x, Decode() tries to read
a length prefix that doesn't exist, causing an EOF error:

    unable to decode features: EOF

The fix adds a deserializeChanEdgeFeatures() helper that detects which
format is being read and decodes accordingly:

- New format (v0.20+): First 2 bytes encode the length of the remaining
  bytes. Detected when uint16(bytes[0:2]) == len(bytes)-2.

- Legacy format (pre-v0.20): Raw feature bits without length prefix.
  Uses DecodeBase256 with the known length.

The format detection is safe because in the legacy format, the first byte
always has at least one bit set (the serialization uses minimum bytes),
so the first two bytes can never encode a value equal to len-2.

Fixes #10528.
2026-02-03 09:12:04 -08:00
Matt Morehouse
5fa025e9e7
docs: add release note 2026-02-03 09:15:34 -06:00
Matt Morehouse
21588acb3d
discovery: fix gossiper shutdown deadlock
When processing a remote network announcement, it is possible for two
error messages to be sent back on the errChan.  Since Brontide doesn't
actually read from errChan, and since errChan only buffered one error
message, the sending goroutine would deadlock forever.  This would only
become apparent when the gossiper attempted to shut down and got hung
up.

For now, we can fix this simply by buffering up to two error messages on
errChan.  There is an existing TODO to restructure this logic entirely
to use the actor model, and we can do a more thorough fix as part of
that work.

This bug was discovered while doing full node fuzz testing and was
triggered by sending a specific channel_announcement message and then
shutting down LND.
2026-02-03 09:14:56 -06:00
Elle Mouton
df0bd94b91
scripts: update bw-compat test LND base version
Update the backwards compat test to use lnd v0.20.0-beta as the base
version.
2026-02-03 17:06:24 +02:00
Yong
06b175a140
Merge pull request #10463 from hieblmi/fix-trickle
discovery: default trickledelay to 1 if less is set
2026-02-03 18:42:30 +08:00
Slyghtning
22feff6dbb
docs: update release notes for gossip trickle delay validation 2026-02-02 20:48:01 +01:00
Slyghtning
4ef2019dd9
config: default TrickleDelay to 1ms if non-positive 2026-02-02 20:47:59 +01:00
Slyghtning
dd29d5690f
sqldb: close test db in cleanup 2026-02-02 18:51:39 +01:00
ziggieXXX
20a6b1726f
Merge pull request #10532 from darioAnongba/fix/ignored-local-mark-coop
peer: fix MarkCoopBroadcasted to correctly use local parameter
2026-02-02 12:36:06 -05:00
Dario Anongba Varela
6e56c9a538
peer: fix MarkCoopBroadcasted to correctly use local parameter 2026-02-02 12:16:29 +01:00
George Tsagkarelis
e532d4e49d
btcwallet: add test coverage for maybeTweakPrivKey 2026-01-30 16:02:15 +01:00
George Tsagkarelis
8125286d30
btcwallet: support combined tweak to private key
Previously we'd define either a single or a double tweak for the sign
descriptor. We introduce the option to apply both consecutively (double
tweak first, single tweak second) if both tweak parameters are set. For
callers who define only one of the two parameters we maintain the old
behavior.
2026-01-30 16:02:15 +01:00
Olaoluwa Osuntokun
7ccab028f0
Merge pull request #10531 from Roasbeef/claude-fix
workflows: fix Claude workflows for fork PRs and external contributors
2026-01-29 16:41:18 -04:00
Olaoluwa Osuntokun
a77b9d9b89 workflows/pr-severity: allow non-write users for classification
The PR severity classifier was failing for external contributors because
the claude-code-action checks that the actor has write permissions. Since
this workflow only reads PR metadata via the API and doesn't execute any
code from the PR (and has restricted tool permissions), it's safe to allow
any user to trigger classification.
2026-01-29 15:57:18 -04:00
Olaoluwa Osuntokun
bcb589cc9e workflows/claude: fix PR checkout for fork PRs
When a PR originates from a fork, the PR branch doesn't exist in the
origin remote. This adds a step that uses `gh pr checkout` before
running the Claude action, which properly handles fork PRs by adding
the fork as a remote and fetching the branch from there.
2026-01-29 13:30:41 -04:00
Olaoluwa Osuntokun
19b2ad7979
Merge pull request #10527 from Roasbeef/fix-pr-severity-fork-prs
workflows/pr-severity: use pull_request_target for fork PRs
2026-01-28 15:53:19 -04:00
Olaoluwa Osuntokun
4d98b004ac workflows/pr-severity: use pull_request_target for fork PRs
Switch from pull_request to pull_request_target to allow the workflow
to run on PRs from forks. The pull_request trigger runs in the fork's
context which cannot access repository secrets.

This is safe because the workflow only reads PR metadata via the GitHub
API (changed files, labels) and doesn't checkout or execute any code
from the PR itself.
2026-01-28 15:18:19 -04:00
Olaoluwa Osuntokun
22c2cc21e8
Merge pull request #10526 from lightningnetwork/pr-severity-workflow
build: add PR severity classification workflow
2026-01-28 14:58:33 -04:00
Olaoluwa Osuntokun
ba52a27860 build: add PR severity classification workflow
Add a GitHub Actions workflow that uses Claude Code to automatically
classify PRs by severity based on the files changed. This helps
reviewers prioritize and understand PR complexity at a glance.

The workflow:
- Triggers on PR open and synchronize events
- Uses Claude Code to analyze changed files against severity mapping
- Applies one of four severity labels (critical/high/medium/low)
- Posts a detailed comment explaining the classification
- Supports manual override via severity-override-* labels

Severity mapping:
- CRITICAL: lnwallet, htlcswitch, contractcourt, peer, keychain, input,
  channeldb, funding, lnwire, server.go, rpcserver.go
- HIGH: routing, invoices, sweep, discovery, graph, watchtower, feature,
  lnrpc, macaroons, chainntnfs, etc.
- MEDIUM: payments, autopilot, lncfg, kvdb, proto files, etc.
- LOW: docs, tests, scripts, CI/CD config
2026-01-28 14:33:15 -04:00
Olaoluwa Osuntokun
349b3dbb43
Merge pull request #10525 from lightningnetwork/add-claude-github-actions-1769542743473
build: add cc integration
2026-01-27 15:40:14 -04:00
Olaoluwa Osuntokun
c000478121 "Claude PR Assistant workflow" 2026-01-27 15:39:04 -04:00
Yong
676386ce52
Merge pull request #10520 from darioAnongba/fix/routerrpc-failuredetail-additions
routerrpc: FailureDetail enums for invoice/AMP validation failures
2026-01-23 09:58:22 +08:00
Dario Anongba Varela
05eed5cdb4
routerrpc: FailureDetail enums for invoice/AMP validation failures 2026-01-22 15:30:19 -05:00
ziggieXXX
8646d98469
Merge pull request #10514 from ziggie1984/reduce-log-noice
Reduce graphDB log noise
2026-01-22 04:13:24 +01:00
ziggie
c78a75f5d8
graphdb: reduce log noise from WRN to DBG 2026-01-21 20:25:08 +01:00
Olaoluwa Osuntokun
aaaf235b2a
Merge pull request #10331 from Roasbeef/cnct-reorg-v2
multi: update close logic to handle re-orgs of depth n-1, where n is num confs - add min conf floor
2026-01-16 14:48:30 -08:00
Olaoluwa Osuntokun
3fb59b1b6b multi: increase min cltv delta to 24 2026-01-16 13:06:30 -08:00
Olaoluwa Osuntokun
4bce4ebe03 routing: increase MinCLTVDelta from 18 to 24 blocks
This increases the minimum CLTV delta allowed for invoice creation to
provide more headroom above DefaultFinalCltvRejectDelta (19 blocks).
The previous value of 18 was below the reject threshold, which could
allow users to create invoices with CLTV deltas that would be rejected
when receiving payments.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
4aef4d00ac docs/release-notes: add release notes entry 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
fa8ba6f81c lncfg: increase DefaultIncomingBroadcastDelta to 16
With this change, we'll go to chain even earlier to ensure that we have
enough time to sweep a potentially contested HTLC, now that we're
waiting longer before sweeps to ensure that the commitment transaction
is sufficeitnyl burried before we sweep.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
ab4f89128f contractcourt: unify+simplify new re-org aware logic 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
ebefe63f8e contractcourt: add sync dispatch fast-path for single confirmation closes
In this commit, we add a fast-path optimization to the chain watcher's
closeObserver that immediately dispatches close events when only a single
confirmation is required (numConfs == 1). This addresses a timing issue
with integration tests that were designed around the old synchronous
blockbeat behavior, where close events were dispatched immediately upon
spend detection.

The recent async confirmation architecture (introduced in commit f6f716ab7)
properly handles reorgs by waiting for N confirmations before dispatching
close events. However, this created a race condition in integration tests
that mine blocks synchronously and expect immediate close notifications.
With the build tag setting numConfs to 1 for itests, the async confirmation
notification could arrive after the test already started waiting for the
close event, causing timeouts.

We introduce a new handleSpendDispatch method that checks if numConfs == 1
and, if so, immediately calls handleCommitSpend to dispatch the close event
synchronously, then returns true to skip the async state machine. This
preserves the old behavior for integration tests while maintaining the full
async reorg protection for production (where numConfs >= 3).

The implementation adds the fast-path check in both spend detection paths
(blockbeat and spend notification) to ensure consistent behavior regardless
of which detects the spend first. We also update the affected unit tests to
remove their expectation of confirmation registration, since the fast-path
bypasses that step entirely.

This approach optimizes for the integration test scenario without compromising
production safety, as the fast-path only activates when a single confirmation
is sufficient - a configuration that only exists in the controlled test
environment.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
bb781bbe84 itest: add new coop close rbf itest
This ensures that during the RBF process, if one confirms, a re-org
occurs, then another confirms, that we'll properly detect this case.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
8f5656e6e2 contractcourt: add generic close re-org tests
In this commit, we add a set of generic close re-org tests. The most
important test is the property based test, they will randomly confirm
transactions, generate a re-org, then assert that eventually we dtect
the final version.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
b6b4cefd08 contractcourt: add unit tests for rbf re-org cases
This set of new tests ensures that if have created N RBF variants of the
coop close transaction, that any of then can confirm, and be re-org'd,
with us detecting the final spend once it confirms deeploy enough.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
e0f7b0a033 contractcourt: update existing chain watcher tests due to new logic
All the tests need to send a confirmation _after_ the spend is detected
now.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
ce779a9707 contractcourt: add new chainWatcherTestHarness
We'll use this for all the upcoming tests.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
0f59756b40 lntest: add new wait for conf helper method to ChainNotifier 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
12668b8af6 contractcourt: update close logic to handle re-orgs of depth n-1, where n is num confs
In this commit, we update the close logic to handle re-ogs up to the
final amount of confirmations. This is done generically, so we're able
to handle events such as: coop close confirm, re-org, breach confirm,
re-org, force close confirm, re-org, etc.

The upcoming set of new tests will exercise all of these cases.

We modify the block beat handling to unify the control flow. As it's
possible we get the beat, then see the spend, or the oher way around.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
6553b61aa4 peer: send out a notification after the 1st conf, then wait for the rest
We wnt to add better handling, but not break any UIs or wallets. So
we'll continue to send out a notification after a single confirmation,
then send another after things are fully confirmed.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
c0f48d23b4 multi: add new ChannelCloseConfs param, thread thru as needed
In this commit, we add a new param that'll allow us to scale up the
number of confirmations before we act on a new close. We'll use this
later to improve the current on chain handling logic.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
622fa4476f lncfg: add new dev config option for scaling channel close confs
This'll be useful for the set up upcoming itests.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
931b54e848 peer+rpcserver: use new conf scaling for notifications 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
4f43717d4f lnwallet: add tests for new conf scaling helper funcs 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
09c813a61c lnwallet: define helper func to coop close conf scaling
We have two versions: for itests, we just use one conf, but in prod,
we'll scale the number of confirmations.
2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
45aa94c760 server: use new FundingConfsForAmounts helper func 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
9e74916118 lnwallet: add new helper functions to scale confirmations based on amt 2026-01-15 16:22:26 -08:00
Olaoluwa Osuntokun
eb7fbd47d3
Merge pull request #10460 from ziggie1984/fix-flake-revoked_uncooperative_close_retribution
itest fix flake in revokedCloseRetributionRemoteHodlCase
2026-01-14 16:51:16 -08:00
ziggieXXX
7be6f45d7a
Merge pull request #10495 from ziggie1984/deprecate-no-experimental-endorsement
lncfg: add deprecated no-experimental-endorsement config option
2026-01-14 16:02:03 +01:00
ziggie
04dab619cb
lncfg: add deprecated no-experimental-endorsement config option
Re-adds the old no-experimental-endorsement config option as a hidden,
deprecated alias for no-experimental-accountability. This ensures
backward compatibility for users who have the old option in their
config files after the rename it.
2026-01-14 09:26:36 +01:00
ziggieXXX
70dfbd284b
Merge pull request #10488 from yashbhutwala/rpcserver-fix-fundmax-maxchansize
rpcserver: use protocol max for fundMax, not maxChanSize
2026-01-13 22:03:14 +01:00
ziggieXXX
0426cf9e4b
Merge pull request #10491 from ziggie1984/bump-golang-toolchain-go-1.25.5
bump golang toolchain go 1.25.5
2026-01-13 20:11:08 +01:00
ziggie
7085f4706d
fn: fix printf vet check in TestSomeToOkf Go 1.24+
See also https://github.com/golang/go/issues/60529. Now we need
to use a constant.
2026-01-13 17:31:24 +01:00
Yong
28355a20b9
Merge pull request #10493 from ziggie1984/add-usetesting-linter
Update usetesting linter to match old behavior
2026-01-13 23:59:06 +08:00
Yash Bhutwala
f48e6ba145
docs: update release notes for fundMax fix 2026-01-13 10:54:55 -05:00
Yash Bhutwala
73e7708f15
itest: add fundMax with maxChanSize test
Add integration test to verify that fundMax uses the protocol-level
maximum channel size instead of the user-configured maxChanSize. The
test uses a table-driven approach to cover both non-wumbo and wumbo
scenarios, creating nodes with restrictive maxChanSize (5M sats) and
verifying that fundMax still creates channels at the protocol maximum.
2026-01-13 10:54:55 -05:00
Yash Bhutwala
0764f27b6f
rpcserver: use protocol max for fundMax, not maxChanSize
The maxChanSize config option is documented to only apply to incoming
channel requests. However, when using fundMax with OpenChannel, the code
was incorrectly using maxChanSize as the upper bound for the outgoing
channel size.

This commit fixes the issue by using the protocol-level maximum
(MaxBtcFundingAmount or MaxBtcFundingAmountWumbo depending on wumbo
support) as the upper bound for fundMax operations.

Fixes #10468.
2026-01-13 10:54:55 -05:00
ziggie
e26a114cdc
mod: update the minimum go version to 1.24.11
We update the minimum required go version to build the LND
exectuable to the latest minor release of go 1.24.11.
2026-01-13 16:07:38 +01:00
ziggie
61d82fd1f2
build: update CI+release version to Go 1.25.5
This updates the toolchain which we require to build the LND
executable.
2026-01-13 16:07:38 +01:00
András Bánki-Horváth
72ab4d234a
Merge pull request #10289 from GeorgeTsagk/move-aux-closer
Aux Closer: Move coop-close aux finalization to chain watcher
2026-01-13 15:31:34 +01:00
ziggie
35260fc4a7
discovery: fix new usetesting linter issues 2026-01-13 13:01:19 +01:00
ziggie
e05ee9a8f5
linter: update usetesting linter to match old behavior 2026-01-13 12:56:42 +01:00
Olaoluwa Osuntokun
ab6ff825c6
Merge pull request #10470 from Roasbeef/discovery-panic-recovery
discovery: add panic recovery for gossip message processing
2026-01-12 17:10:35 -08:00
Olaoluwa Osuntokun
7da41cb367
docs/release-notes: add release notes 2026-01-12 17:10:17 -08:00
Olaoluwa Osuntokun
bcb65f5ac7
discovery: add panic recovery for serial announce signatures processing
In this commit, we extend the panic recovery mechanism to cover the
serial processing path for AnnounceSignatures1 messages. Unlike other
gossip messages which are processed in parallel goroutines, announcement
signatures are processed serially in the main networkHandler loop.

A panic during this serial processing would previously crash the entire
gossiper. This change wraps the processing in an anonymous function with
a deferred panic recovery, ensuring resilience without changing the
serial processing semantics.

Since AnnounceSignatures bypass the validation barrier, we pass nil for
the jobID parameter.
2026-01-12 17:10:17 -08:00
Olaoluwa Osuntokun
caf4850f74
discovery: add panic recovery for gossip message processing
In this commit, we add a centralized panic recovery mechanism for gossip
goroutines. This increases the robustness of message processing in the
gossiper, as now we are able to keep on trucking in the face of logic
errors that may lead to panics.

We ensure that any deps are freed and we log the panic trace to help
catch bugs in the future.
2026-01-12 17:10:00 -08:00
ziggieXXX
7bbf26c0e2
Merge pull request #10469 from Roasbeef/lnwire-timestamp-validation
lnwire: enforce non-zero timestamp in gossip messages
2026-01-12 13:57:12 +01:00
Olaoluwa Osuntokun
cad1b957bf
discovery: enforce non-zero timestamp in gossip messages
In this commit, we add validation for channel updates and node
announcements to ensure that we reject gossip messages with zero
timestamps at the discovery layer.

From BOLT 7:
  "MUST set timestamp to greater than 0, AND to greater than any
  previously-sent channel_update for this short_channel_id."

This validation is performed in the gossip handlers (handleNodeAnnouncement
and handleChanUpdate) rather than at the wire protocol level. This approach
ensures we can still decode messages from disk or embedded in onion errors
while rejecting invalid gossip from peers.

Remote peers sending zero-timestamp gossip will have their ban score
incremented.
2026-01-09 17:10:37 -08:00
ziggieXXX
39a1421d15
Merge pull request #10296 from hieblmi/estimate-inputs
estimatefee: tx fee estimate for selected inputs
2026-01-07 11:45:24 +01:00
Slyghtning
202e473c7f
docs: update release notes 2026-01-07 10:30:04 +01:00
Slyghtning
422aa7d182
itest: estimate on-chain fee with selected inputs 2026-01-07 10:30:03 +01:00
Slyghtning
44aa5efc66
cmd: EstimateFee for explicit inputs 2026-01-07 10:25:30 +01:00
Slyghtning
280929ba1e
rpcserver: return inputs in EstimateFeeResponse 2026-01-07 10:25:29 +01:00
Slyghtning
e5456d39f2
cmd: make types.go accessible in lnd package 2026-01-07 10:25:29 +01:00
Slyghtning
049f2c738d
lnrpc: transaction inputs in EstimateFeeRequest 2026-01-07 10:25:28 +01:00
ziggieXXX
d5d151cee9
Merge pull request #10462 from ziggie1984/fix-prune-node-race
channeldb: fix race condition in link node pruning
2026-01-07 08:54:33 +01:00
ziggieXXX
99d07fdb78
Merge pull request #10367 from elnosh/rename-endorsement-accountable
multi: rename experimental endorsement signal to accountable
2026-01-06 18:02:22 +01:00
Yong
d3ce622bde
Merge pull request #10403 from gijswijs/fix-flatmap
Fix FlatMap implementation in Result type
2026-01-06 22:37:18 +08:00
elnosh
6f49bea52b server: remove accountability signal experiment period
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.
2026-01-06 09:12:34 -05:00
elnosh
34329c684e multi: rename experimental endorsement signal to accountable
Renames the endorsement signal to accountable to
match the latest proposal https://github.com/lightning/blips/pull/67
2026-01-06 09:12:31 -05:00
ziggie
d9fb9092b6
multi: make sure previous inconsitent states are fixed
We make sure that nodes previously suffering from this error will
have a consitent db view when restarting their node.
2026-01-06 14:34:52 +01:00
ziggie
51f3c6f528
channeldb: fix race condition in link node pruning
This commit fixes a critical race condition in MarkChanFullyClosed and
pruneLinkNode where link nodes could be incorrectly deleted despite
having pending or open channels.

The race occurred because the check for open channels and the link node
deletion happened in separate database transactions:

  Thread A: TX1 checks open channels → [] (empty)
  Thread A: TX1 commits
  Thread B: Opens new channel with same peer
  Thread A: TX2 deletes link node (using stale data)
  Result: Link node deleted despite pending channel existing

This creates a TOCTOU (time-of-check to time-of-use) vulnerability where
database state changes between reading the channel count and deleting
the node.

Fix for MarkChanFullyClosed:
- Move link node deletion into the same transaction as the channel
  closing check, making the check-and-delete operation atomic

Fix for pruneLinkNode:
- Add double-check within the write transaction to verify no channels
  were opened since the caller's initial check
- Maintains performance by keeping early return for common case
- Prevents deletion if channels exist at delete time

This ensures the invariant: "link node exists iff channels exist"
is never violated, preventing database corruption and potential
connection issues.
2026-01-06 10:52:41 +01:00
Gijs van Dam
e7b9515edd fn: fix Result[T].FlatMap
For a Result[T], FlatMap should apply f when the result is Ok, and
propagate the error unchanged when it's Err. The original code returns r
on Ok and tries to use r.left when Err, which is wrong. This commit
fixes that.

Secondly, the group of FlatMap/AndThen and OrElse functions and methods
are now properly tested with new unit tests.

fixes #10401
2026-01-06 10:51:49 +01:00
ziggieXXX
801de79cf1
Merge pull request #10476 from ziggie1984/fix-endorsement-testcases
itest: fix endorsement itests
2026-01-05 16:57:46 +01:00
ziggie
5f30797738
itest: fix endorsement itests
Due to the signaling period expiring tests had to be adopted bc
they were not taking the activation time period into account.
2026-01-05 14:28:14 +01:00
ziggieXXX
ac006d73cf
Merge pull request #10356 from Abdulkbk/ispublicnode-perf
graph: fix inefficient query for IsPublicNode
2026-01-05 10:32:56 +01:00
ziggieXXX
eea4840086
Merge pull request #10249 from erickcestari/fix-scid-tlv-length
lnwire+tlv+route: enforce TLV length validation and add tests
2025-12-30 19:31:32 +01:00
Erick Cestari
523002984b
docs: add release-notes 2025-12-29 19:19:53 -03:00
Erick Cestari
5a7925008e
multi: enforce strict TLV length checks
This commit improves TLV decoding safety and consistency across multiple
packages by enforcing fixed-length requirements and adding unit tests to
prevent malformed TLV records from being accepted.

Changes include:

- lnwire:
  * Enforce 8-byte length in Fee TLV decoder.
  * Enforce PubNonceSize in Musig2Nonce TLV decoder.
  * Enforce 8-byte length in ShortChannelID TLV decoder.
  * Added roundtrip and invalid length tests for Fee, Musig2Nonce,
    and ShortChannelID records.

- routing/route:
  * Enforce Vertex TLV length (33 bytes).
  * Added encode/decode and invalid length tests for Vertex.

- tlv:
  * Enforce correct length in DBytes33 decoder (33 bytes).
  * Added tests ensuring all fixed-size primitive decoders reject
    incorrect TLV lengths.

By strictly validating TLV lengths, we prevent malformed or corrupted
TLV records from being silently accepted, improving protocol safety.
2025-12-29 19:18:49 -03:00
ziggie
4d1faabd08
peer: fix log output when not applicable
Before we would always log that the peer was not ready starting
up although it was not the case. We now make sure we still log
this case but only when applicable.
2025-12-25 11:35:19 +01:00
Abdullahi Yunus
68f558c865
docs: add release note 2025-12-24 18:08:46 +01:00
Abdullahi Yunus
ac2cec462c
graph: use UNION for isPublicNode query
In this commit we updated the IsPublicV1Node query to use UNION
instead of OR, since sqlite struggles to efficiently use
multiple indexes in a single query involving OR conditions across
different columns.

We use UNION ALL since the query doesn't care about duplicates.
2025-12-23 21:04:36 +01:00
Abdullahi Yunus
86cde4b93f
graphdb: add benchmark for isPublicNode query
In this commit we add a benchmark to test the performance of
IsPublicNode query.
2025-12-23 21:04:36 +01:00
Yong
013f5c93ed
Merge pull request #10455 from ziggie1984/fix-onchain-timeout-terminal
Fix flaky itest multihop testcase
2025-12-22 23:10:09 +08:00
ziggie
ece8469051
lntest: make sure HTLCs are locked in when sending a payment
Before this change, CompletePaymentRequestsNoWait would return as
soon as the channel's NumUpdates increased by at least one. When
sending multiple payments, this meant the function could return
while some HTLCs were still in-flight and not yet committed to the
channel state.

The problem occurred when tests captured the channel state
immediately after calling this function. Even though we read the
current NumUpdates from the channel, HTLCs could still be in the
process of being committed. This led to a race where the channel
would progress to a new state after we thought we had correctly
captured it, causing tests to see unexpected commitment heights.

Fix this by waiting for all outgoing HTLCs to appear in
PendingHtlcs before returning. We count outgoing HTLCs before
sending, then wait until exactly len(paymentRequests) new HTLCs
are present. This guarantees all payments have fully completed
their commitment exchange and are locked in on both sides.

Fixes the flaky revokedCloseRetributionRemoteHodlCase test where
backups would capture state at height N+1 instead of the expected
  height N.
2025-12-21 21:52:22 +01:00
ziggie
c493f7d9dd
itest: accept NO_ROUTE or TIMEOUT in testLocalClaimOutgoingHTLC
Fix flaky test due to race between channel closure propagation and
payment timeout. Both failure reasons are correct depending on timing.
2025-12-19 00:27:20 +01:00
Yong
91423ee519
Merge pull request #10449 from ziggie1984/bugfix/setsource-node-sql
server: fix timestamp comparison in setSelfNode
2025-12-16 16:58:25 +08:00
ziggie
e5b0704d73
docs: add release-notes for LND 20.1 2025-12-16 08:21:11 +01:00
ziggie
865e1556d4
server: fix timestamp comparison in setSelfNode
Fix bug where setSelfNode compared only the seconds component of
timestamps instead of the full timestamp. This caused the node to
attempt persisting an older timestamp than what existed in the
database during restart, resulting in "sql: no rows in result set"
errors.
2025-12-16 08:21:11 +01:00
Olaoluwa Osuntokun
04f6108051
Merge pull request #10446 from ziggie1984/bugfix/backport-workflow
ci: fix some minor issues in the backport ci job
2025-12-15 10:59:36 -08:00
ziggieXXX
06cc0f3aa3
Merge pull request #10448 from ziggie1984/bugfix/fix-comment-lsp-heuristic
routerrpc: fix comment in lsp heuristic
2025-12-15 15:10:09 +01:00
Yong
0cc84d0b00
Merge pull request #10353 from starius/fix-rbf_coop_test
chancloser: stabilize remote RBF coop tests
2025-12-15 14:56:00 +08:00
Boris Nagaev
d47d86ce0c
chancloser: stabilize remote RBF coop tests
CI started panicking in TestRbfChannelFlushingTransitions/early_offer /
TestRbfCloseClosingNegotiationRemote (see GitHub Actions run
https://github.com/lightningnetwork/lnd/actions/runs/19155841408/job/54756127218?pr=10352)
because the cached remote offer could fire before the test harness
registered its mock CloseSigner expectations. When that happened,
the mock complained that CreateCloseProposal was unexpected:

panic:
assert: mock: I don't know what to return because the method call was unexpected.

Fix this by having assertSingleRemoteRbfIteration take a sendEvent callback
that receives the context and initial offer, so tests can install expectations
first and then fire the event via SendEvent (or the early-offer test's custom
flush sender).

Reproduction (on master)
------------------------
1. Modify lnwallet/chancloser/rbf_coop_test.go
   Add time.Sleep(10 * time.Millisecond) before the first call of
   closeHarness.assertSingleRemoteRbfIteration (in function
   TestRbfChannelFlushingTransitions).

2. go test ./lnwallet/chancloser -run TestRbfChannelFlushingTransitions/early_offer

3. The panic reproduces immediately.
2025-12-14 20:03:43 -03:00
ziggie
5580c40cb0
routerrpc: fix comment in lsp heuristic 2025-12-14 09:25:30 +01:00
ziggie
dbb3a9e953
ci: fix some minor issues in the backport ci job
-Due to a newer version we need to use add_labels instead of just
 labels

-The backport PR will now also copy the milestones in case the
 milstones were set
2025-12-14 09:00:02 +01:00
ziggieXXX
9fcd7c2972
Merge pull request #10435 from ziggie1984/test-backport-workflow
Backport workflow
2025-12-13 00:16:24 +01:00
ziggie
4af13745e5
docs: add comprehensive backport workflow documentation
This commit adds detailed documentation for the automated backport
workflow and updates the contribution guidelines to reference it.

New documentation (docs/backport-workflow.md):
- Complete overview of the automated backport process
- Step-by-step usage instructions with examples
- Detailed explanation of workflow triggers and label format
- Technical details about workflow implementation
- Conflict resolution procedures and best practices
- Multiple backport scenarios and examples
- Comprehensive troubleshooting guide

Updated contribution guidelines (docs/code_contribution_guidelines.md):
- Replaced detailed backport instructions with brief overview
- Added reference to the new detailed documentation
- Keeps contribution guidelines focused and concise

The detailed documentation provides:
- How to use backport labels correctly
- What happens when labels are added before/after merge
- How the workflow validates branches and handles errors
- Step-by-step conflict resolution instructions
- Solutions for common problems and edge cases
- Examples of valid vs invalid label formats

This documentation ensures contributors and maintainers have clear
guidance on using the automated backport workflow effectively.
2025-12-12 22:48:51 +01:00
ziggie
7ecdb9b807
ci: add automated backport workflow
This commit introduces an automated GitHub Actions workflow to streamline
the backporting process for merged PRs from master to release branches.

Key features:
- Triggers on merged PRs with labels matching 'backport-v*' pattern
  (e.g., backport-v0.20.x-branch)
- Validates that target branches exist before attempting backport
- Creates separate backport PRs for each target branch
- Automatically adds 'no-changelog' label to backport PRs
- Handles merge conflicts by creating draft PRs with conflict markers
- Supports multiple simultaneous backports via multiple labels

Workflow steps:
1. Checkout repository with full git history
2. Validate all target branches exist in the remote repository
3. For each valid backport label:
   - Create a new branch (backport-<pr-num>-to-<target-branch>)
   - Cherry-pick commits from the master PR
   - Create a new PR targeting the release branch
   - Link back to the original PR
4. If conflicts occur, create a draft PR for manual resolution

Label format:
- Valid: backport-v0.20.x-branch, backport-v0.19.x-branch
- Invalid: backport candidate, backport-candidate, backport-needed

This automation reduces manual work and ensures consistency in the
backporting process while maintaining full visibility and control
for maintainers.
2025-12-12 22:48:51 +01:00
Elle
363aeb5f15
Merge pull request #10410 from ellemouton/bypassBuilderForMissingEdges
localchans: populate FundingScript for missing edges
2025-12-12 10:22:14 +02:00
ziggieXXX
f4b3ed73e3
Merge pull request #10440 from ziggie1984/bugfix/itest-race
funding: fix race in itest for zero-conf funding
2025-12-12 00:05:27 +01:00
Elle Mouton
da9868b06d
docs: update release notes 2025-12-11 18:13:47 +02:00
Elle Mouton
710c676662
localchans: populate funding script for missing edges
When creating a missing edge, we need to populate the funding script too
so that the graph builder can update its ChainView appropriately. We use
the MakeFundingScript helper from the funding package which ensures that
we are using the same logic for creating a funding script as is used for
any of the channels that we own.
2025-12-11 18:13:47 +02:00
Elle Mouton
66bc0b8f8e
funding: export MakeFundingScript
So that we can re-use this helper else where.
2025-12-11 17:54:57 +02:00
ziggie
fe9a3e103b
funding: fix race in itest for zero-conf funding
Fix a race condition where forwarding through a public zero-conf channel
could fail with UnknownNextPeer when using the confirmed SCID. The issue
occurred because ReportShortChanID (which updates the switch's baseIndex
to handle the confirmed SCID) was called AFTER addToGraph (which announces
the confirmed SCID to the network).

With slow backends like postgres, addToGraph takes significant time,
creating a window where other nodes learn about the confirmed SCID from
gossip and attempt to route through it, but the receiving node's switch
hasn't been updated yet to handle forwards using the confirmed SCID.

The fix reorders operations to call ReportShortChanID before addToGraph,
ensuring the switch is ready to handle the confirmed SCID before it's
announced to the network. Forwards using either the alias or confirmed
SCID will work since getLinkByMapping uses baseIndex to map both to the
same link in forwardingIndex.

Fixes flaky test: zero_conf-channel_policy_update_public_zero_conf
2025-12-10 19:44:18 +01:00
Yong
1e15efc473
Merge pull request #10439 from ziggie1984/bugfix/probing
Always add the payment address when probing an invoice
2025-12-10 22:39:58 +08:00
ziggie
ac30443cc1
docs: add release-notes for LND 20.1 2025-12-10 09:49:57 +01:00
ziggie
1c4bcc3b7d
routerrpc: fix payment address deep copy
using copy for a slice of size 0 will not copy anything so we need
to first initialize the slice before we do the deep copy.
2025-12-10 09:49:57 +01:00
Elle
3d7ff866f8
Merge pull request #10436 from ellemouton/musigRegisterAggNonce 2025-12-10 09:58:58 +02:00
Elle Mouton
a7b61f3ec1
docs: add release notes for combined nonce RPC methods
Document the new MuSig2RegisterCombinedNonce and MuSig2GetCombinedNonce RPC
methods in the v0.21.0 release notes. These methods enable coordinator-based
signing patterns as an alternative to the standard MuSig2RegisterNonces
workflow.
2025-12-10 07:19:07 +02:00
Elle Mouton
fb48697ffa
itest+lntest: add coordinator pattern test for combined nonce
Add integration test for MuSig2RegisterCombinedNonce and
MuSig2GetCombinedNonce RPCs to verify the coordinator pattern workflow.

The test:
- Creates three signing sessions without initial nonce exchange
- Manually aggregates nonces using the coordinator pattern (btcec musig2)
- Tests v0.4.0 returns unsupported errors (as expected)
- Tests v1.0.0rc2 successfully registers and retrieves combined nonces
- Verifies mutual exclusivity (error: already have all nonces)
- Completes a full signing flow to ensure signatures are valid

Also adds the required RPC harness wrapper methods to lntest/rpc/signer.go for
the new RPCs and adds MuSig2RegisterNoncesErr wrapper for error testing.
2025-12-10 07:19:04 +02:00
Elle Mouton
16b9192dad
signrpc: implement combined nonce RPC server handlers
Add server-side RPC handlers for MuSig2RegisterCombinedNonce and
MuSig2GetCombinedNonce.

The handlers:
- Delegate to the Signer interface methods
- Validate input (session ID format, combined nonce length)
- Include macaroon permissions (generate for register, read for get)

These handlers complete the server-side RPC implementation.
2025-12-10 07:15:06 +02:00
Elle Mouton
c945f6749e
input+signrpc+lnwallet+multi: implement combined nonce support
Add CombinedNonce() and RegisterCombinedNonce() methods with full implementation
stack.

Interface and core implementation:
- input/musig2.go: Added methods to MuSig2Session and MuSig2Signer interfaces
- input/musig2_session_manager.go: MusigSessionManager implementation using
  HaveAllNonces flag for state tracking (simplified, no extra fields)
- internal/musig2v040: Stub implementations returning ErrUnsupportedMethod
- Mock implementations (MockInputSigner, MockSigner, DummySigner)

RPC layer:
- lnrpc/signrpc/signer.proto: RPC method definitions and messages
- lnrpc/signrpc/signer.yaml: REST API endpoint mappings
- Generated protobuf code (all .pb.go files)
- lnwallet/rpcwallet/rpcwallet.go: RPCKeyRing client implementation

The proto types and RPCKeyRing are added together since RPCKeyRing implements
the Signer interface and requires proto types to fulfill the contract.

For v0.4.0, these methods return ErrUnsupportedMethod. Use MuSig2Version100RC2
to access these features.
2025-12-10 07:15:06 +02:00
Elle Mouton
cbcb6a4dcf
go.mod: update btcec dep
To include the update to the musig2 Session which allows the aggregate
nonce for the session to be registered instead of requiring the
individual nonces to be registered.
2025-12-10 07:14:59 +02:00
Olaoluwa Osuntokun
456d7dcf05
Merge pull request #10424 from MPins/sat_per_byte_remove_warning
docs: release-notes-0.21
2025-12-09 17:59:15 -08:00
Olaoluwa Osuntokun
ea5466a127
Merge pull request #10412 from Roasbeef/faster-linter
make: use Docker named volumes for ~21x faster local linting (Mac OS)
2025-12-09 17:45:16 -08:00
Olaoluwa Osuntokun
a21d436fb2
make: use Docker named volumes for ~21x faster local linting
This commit optimizes Docker cache mounting for the linter with a
CI-aware strategy:

**Local development (macOS/Windows)**: Uses Docker named volumes which
keep data inside Docker's native Linux filesystem, avoiding the slow
host-syncing overhead of bind mounts. This yields ~21x faster linting
on warm cache.

**CI (GitHub Actions)**: Uses bind mounts to host paths (`~/.cache/go-build`,
`~/go/pkg/mod`) that GitHub Actions already caches via the setup-go
action. This ensures CI benefits from cached dependencies across runs.

The Makefile detects CI mode via the `CI` environment variable that
GitHub Actions sets automatically.

Local benchmark results:
- Cold run (empty cache): ~2m 28s
- Warm run (cached): ~11s (~21x faster)

Key improvements in warm runs:
- Go packages loading: 1m 58s → 5.6s
- Linters execution: 20.5s → 2.7s
- Total execution: 2m 20s → 8.6s
2025-12-09 17:44:47 -08:00
Yong
cb3991ea6e
Merge pull request #10428 from ziggie1984/fix-sql-pool-exhaustion
graphdb: fix potential sql tx exhaustion
2025-12-09 21:09:50 +08:00
ziggie
f289e4920e
docs: add release-notes for LND 20.1 2025-12-09 11:51:47 +01:00
ziggie
2d25bce1bf
graphdb: fix potential sql tx exhaustion
We should avoid taking the lock of a mutex inside transaction.
Currently we also take this lock in other places and there is a
chance that in case the application lock aquires the lock but
all transactions are already blocked waiting for the mutex to
unlock, we end up in a deadlock.
2025-12-09 11:51:46 +01:00
Elle
bc670fab82
Merge pull request #10396 from ziggie1984/enhance-lsp-heuritic
Enhance Lsp Heuristic when probing a payment
2025-12-09 10:43:43 +02:00
MPins
bf473431bd
docs: release-notes-0.21
Warning message about removing the deprecated option --sat_per_byte
2025-12-08 14:31:37 -03:00
Yong
0a6907d44b
Merge pull request #10415 from yyforyongyu/race-test-sql
github: add `unit-race` for sql tests
2025-12-08 22:40:18 +08:00
yyforyongyu
57448a90c8
github: add unit-race for sql tests 2025-12-08 19:31:56 +08:00
Yong
d962b146b0
Merge pull request #10420 from ellemouton/removePubKeyCaching
graph: fix various races
2025-12-08 19:31:32 +08:00
Elle Mouton
e68bf6c58d
docs: add release notes for race condition fixes 2025-12-08 11:57:01 +02:00
Elle Mouton
9f715555c4
graph/db: fix race in DisconnectBlockAtHeight cache access
The DisconnectBlockAtHeight method was modifying the rejectCache and
chanCache without holding the cacheMu lock. This caused races with
other operations that properly held the lock, such as AddChannelEdge
which modifies the caches in its OnCommit callback while the batch
scheduler holds cacheMu.

Fix by acquiring cacheMu before removing channels from the caches.
2025-12-08 11:56:36 +02:00
Elle Mouton
31b3e7424d
graph/db/models: fix race conditions in ChannelEdgeInfo
Both NodeKey1 and NodeKey2 methods had the same race condition as the
Node.PubKey method, where concurrent calls could race to write to the
cached fields.

Remove the caching for the same reasons: parsing overhead is minimal
and doesn't justify the complexity and race risk.
2025-12-08 11:56:36 +02:00
Elle Mouton
9906e61774
graph/db/models: fix race condition in Node.PubKey
The PubKey method had a race condition where concurrent calls could
all pass the nil check and race to write to the cached pubKey field.
This is a classic check-then-act race.

Remove the caching entirely to fix the race. The overhead of parsing
a public key is minimal and doesn't justify the added complexity and
race risk of caching.
2025-12-08 11:56:35 +02:00
Yong
a76f22da9d
Merge pull request #10419 from AbelLykens/patch-2
[docs] Document use-native-sql=true for SQL migration step 2
2025-12-04 19:27:30 +08:00
George Tsagkarelis
2f8697152b
lnd: provide aux closer to chain arbitrator
The final step is to provide the aux closer to the corresponding configs
from the server.
2025-12-04 12:18:46 +01:00
George Tsagkarelis
9a88999c41
contractcourt+lnwallet: move aux close finalization to chain watcher
We now execute the aux chan closer finalization within the chain
watcher. This is better as we don't need to rely on the remote party
being online and sending us a message. Instead we do the finalization
once the on-chain transaction has been confirmed.
2025-12-04 12:18:14 +01:00
George Tsagkarelis
fd772d45b8
contractcourt: add aux chan closer to chain watcher 2025-12-04 12:17:40 +01:00
George Tsagkarelis
e12008517e
lnwallet+peer: extract close types to separate pkg
The aux close types will soon be used by a different package that would
otherwise cause an import cycle if used directly from
lnwallet/chancloser. We now create a new sub-package lnwallet/types that
will be improrted from all users of these types.
2025-12-04 12:16:54 +01:00
AbelLykens
f811805c65 docs: update sql migration guide 2025-12-04 09:34:58 +00:00
Elle
20473482d4
Merge pull request #10371 from ellemouton/fixSourceNodeSetting
graph/db: fix SetSourceNode no rows error
2025-12-03 15:34:41 +02:00
Elle Mouton
3e0bc6b18c
docs: add release note 2025-12-03 13:09:22 +02:00
Elle Mouton
c04aa655b9
graph/db: fix SetSourceNode race with lenient upsert
This commit fixes a race condition where multiple goroutines call
SetSourceNode concurrently during startup, causing sql.ErrNoRows
errors. The race occurs when multiple code paths (setSelfNode,
createNewHiddenService, RPC updates) read the same old timestamp,
independently increment it to the same new value (T+1), and race to
write.

The fix uses the new UpsertSourceNode SQL query (without strict
timestamp constraint) instead of UpsertNode. This allows
last-write-wins semantics for our own node, ensuring all parameter
changes persist even when timestamps collide.

Refactored sql_store.go for reusability:
- upsertNodeAncillaryData: common logic for features/addresses/extras
- populateNodeParams: common parameter building with callback pattern
- buildNodeUpsertParams: builds params for strict UpsertNode
- buildSourceNodeUpsertParams: builds params for lenient UpsertSourceNode
- upsertSourceNode: new function using lenient query

Updated TestSetSourceNodeSameTimestamp to verify that concurrent
updates with the same timestamp now succeed and parameter changes
persist.

Fixes the itest error:
"unable to upsert source node: upserting node(...): sql: no rows in
result set"
2025-12-03 13:08:31 +02:00
Elle Mouton
8349a6f84d
sqldb: add UpsertSelfNode query
This query is less strict in terms of the latest update timestamp field.
We want to be less strict with our own node data since we always want
our own updates recorded.
2025-12-03 13:08:31 +02:00
Elle Mouton
41615f74a5
graph/db: add test for SetSourceNode same timestamp behavior
This commit adds TestSetSourceNodeSameTimestamp to demonstrate the
current behavior when SetSourceNode is called with the same last update
timestamp. The test reveals a difference between the SQL and bbolt
implementations:

- SQL store returns sql.ErrNoRows when attempting to update with the
  same timestamp, as the upsert query's UPDATE clause requires the new
  timestamp to be strictly greater than the existing one
- bbolt store silently ignores stale updates and returns no error

This behavior is important to document because our own node
announcements may change quickly with the same timestamp, unlike
announcements from other nodes where same timestamp typically means
identical parameters.
2025-12-03 13:08:30 +02:00
Yong
4b1c4998ad
Merge pull request #10399 from mohamedawnallah/handle-partial-tls-files
tls_manager: Handle partial TLS files
2025-12-03 18:27:39 +08:00
Mohamed Awnallah
0a6f69dd7e docs: update release notes 2025-12-03 09:13:23 +00:00
Olaoluwa Osuntokun
140248ba9d
Merge pull request #10383 from ziggie1984/bugfix/fix-mission-control-startup
routing: allow misson control manager to startup despite errors
2025-12-02 16:19:15 -08:00
ziggie
17b77b64a3
docs: add release-notes for LND 20.1 2025-12-02 20:38:55 +01:00
ziggie
f6bc88f348
routing: allow misson control manager to startup despite errors
We now allow the mission control manager to skip over deserializable
errors. We cannot repair this these results but we just skip over
it so we can startup properly.

When fetchAll() encounters entries that fail to deserialize, in
addition to skipping them, now also:

- Delete the corrupted entries from the database
- Remove them from the in-memory keysMap and keys tracking structures

This prevents corrupted entries from:
- Being counted toward maxRecords, which would cause valid entries
  to be pruned prematurely
- Persisting in the database indefinitely
- Causing inaccurate entry counts in startup logs
2025-12-02 20:38:55 +01:00
ziggie
de0424eca8
docs: add release-notes for LND 20.1 2025-12-01 18:07:57 +01:00
ziggie
34619d4758
docs: update api documentation for estimateRouteFee 2025-12-01 18:07:56 +01:00
ziggie
0e6bab1730
itest: enhance testEstimateRouteFee with multi-LSP scenarios
This commit enhances the integration test to validate the LSP heuristic
end-to-end with real network topology and payment probing.

Network topology additions:
- Added Frank node as a private destination
- Created multi-LSP test scenario with Bob, Eve, and Dave as LSPs

New test cases:

1. "probe based estimate, public target with public hop hints"
   - Validates Rule 1: public invoice target routes directly
   - Even with public hop hints, direct routing is used
   - Expected: standard single-hop fees

2. "probe based estimate, multiple different public LSPs"
   - Validates multi-LSP worst-case selection
   - Frank has routes through Bob (low fee), Eve (HIGH fee), Dave (medium)
   - Expected: Eve's worst-case fees (most expensive)
   - Tests griefing protection (max 3 LSP probes)
2025-12-01 18:07:56 +01:00
ziggie
79fe67619b
routerrpc: implement LSP heuristic and multi-LSP worst-case probing
This commit implements a comprehensive LSP (Lightning Service Provider)
detection heuristic and updates the payment probing logic to handle
multiple LSPs with worst-case fee estimation.

Key changes:

1. LSP Detection Heuristic (isLSP function):
   Implements three rules to detect LSP setups:
   - Rule 1: If invoice target is public → NOT an LSP (route directly)
   - Rule 2: If at least one destination hop is public → IS an LSP
   - Rule 3: If all destination hops are private → NOT an LSP

2. LSP Route Preparation (prepareLspRouteHints function):
   - Groups route hints by unique public LSP nodes
   - Filters out non-LSP routes based on the heuristic
   - Tracks worst-case fees and CLTV delays for each LSP
   - Returns adjusted route hints with LSP hop stripped

3. Multi-LSP Probing (probePaymentRequest updates):
   - Probes up to 3 unique LSPs maximum (griefing protection)
   - Selects the WORST-CASE (most expensive) route for conservative
     fee estimation
   - Adds comprehensive debug logging for worst-case selection process
   - Properly formats vertex logging using %v (calls Vertex.String())

The worst-case approach ensures users won't be surprised by higher fees
when the actual payment is sent, providing a more conservative and
reliable fee estimate.

This commit also  adds extensive unit test coverage for the LSP detection
heuristic and route preparation logic.

TestIsLsp:
- Edge cases: empty route hints, nil scenarios
- Rule 1: Public invoice target (3 tests)
- Rule 2: All private destination hops (4 tests)
- Rule 3: At least one public destination hop (6 tests)

TestPrepareLspRouteHints:
- LSP grouping and filtering logic
- Worst-case fee selection across route hints
- Worst-case CLTV delta tracking
- Adjusted route hints validation (LSP hop stripped)
- Multi-LSP scenarios with different fees
2025-12-01 18:07:56 +01:00
ziggie
48da56b285
routerrpc: add HasNode backend function for LSP heuristic
This commit adds the HasNode function to the RouterBackend struct,
which checks if a node exists in the graph (i.e., has public channels).
This function is needed by the LSP detection heuristic to determine
if a node is publicly reachable.

The function is wired up in rpcserver.go to query the graph database.
2025-11-29 01:04:28 +01:00
ziggie
06886e71b2
graph/db: fix HasNode comment
The comment was incorrectly referring to HasLightningNode but the
function is named HasNode. Update the comment to match the actual
function name.
2025-11-29 01:04:25 +01:00
Yong
0a2a5b29c7
Merge pull request #10394 from ziggie1984/add-back-globallock
lncfg: readd global lock for postgres and the channeldb_kv table
2025-11-28 20:19:40 +08:00
ziggie
565633578f
docs: add release-notes for LND 20.1 2025-11-28 09:22:20 +01:00
ziggie
4f051d97ea
lncfg+scripts: use configurable global lock for postgres backends
Replace hardcoded WithGlobalLock assignment with configurable
options wallet postgres backends. Also add the WithGlobalLock
option to the channeldb table for postgres backends.

Defaults:
- channeldb: false (allow concurrent access)
- wallet: true (maintain safe single-writer behavior)

Users can now override these defaults via:
- db.postgres.channeldb-with-global-lock
- db.postgres.walletdb-with-global-lock

This gives operators flexibility while maintaining safe defaults
until full native SQL migration is complete.

Moreover exclude db.postgres.walletdb-with-global-lock check
in the sample config file script. We cannot easily check the
correct default because we set it later in the LND startup
sequence so we exclude it.
2025-11-28 09:22:19 +01:00
ziggie
2e16efed30
sqldb: add global lock config options for postgres
Add two configuration options to control global lock usage for
different postgres database backends:

- ChannelDBWithGlobalLock: for channeldb access (default: false)
- WalletDBWithGlobalLock: for wallet database access (default: true)

These allow fine-grained control over which databases use global
locks, rather than hardcoding the behavior. This is a temporary
measure until the revocation log and wallet are migrated to native
SQL and become fully concurrent-safe.
2025-11-28 09:22:19 +01:00
Thiago Romão Barcala
c7fe6425f3 tls_manager.go: handle case when either TLS pair files exist 2025-11-27 13:33:14 +00:00
Mohamed Awnallah
c9dea6d621 tls_manager_test.go: reproduce partial tls files handling
When there is only one of the tls pairs (key/certificate) and the
other is missing, the TLS manager currently assumes it exists
and ignore generating them. This results in error propgated to user
that the other tls pair file is missing/not found.
2025-11-27 13:33:06 +00:00
Yong
85a5bf294c
Merge pull request #10341 from bitromortac/2511-fix-tor-healthcheck
server: prevent duplicate onion addresses in getinfo
2025-11-27 18:40:27 +08:00
bitromortac
b513efc5c1
docs: add release-notes for lnd v0.20.1 2025-11-26 16:58:45 +01:00
bitromortac
031903f275
server: ensure unique addresses for node ann
Modifiers of the node announcement may add duplicate addresses, which we
remove here after the modifications were applied. This also ensures that
any previously added duplicate addresses are removed as well.
2025-11-26 16:56:07 +01:00
Yong
c746aeed31
Merge pull request #10391 from ffranr/wip/add-field-committxblockheight-to-resolutionreq
lnwallet: add field `CommitTxBlockHeight` to `ResolutionReq`
2025-11-26 10:50:53 +08:00
ffranr
3d2b0d703e
lnwallet: add field CommitTxBlockHeight to ResolutionReq
Introduce `CommitTxBlockHeight` field to the `ResolutionReq` structure
and related methods. This field records the block height where a
commitment transaction has confirmed.
2025-11-25 17:06:18 +00:00
Olaoluwa Osuntokun
7e70e3dbd8
Merge pull request #10387 from ziggie1984/fix-scanning-issue
fix scanning issue
2025-11-24 10:09:48 -08:00
ziggie
db3add1c9c
docs: add release-notes for LND 20.1 2025-11-22 01:19:44 +01:00
ziggie
79b92a9fa3
contractcourt: use confheight instead of rescanning the chain 2025-11-22 01:19:43 +01:00
ziggie
5538803252
contracourt: fix comment 2025-11-22 01:17:38 +01:00
ziggie
8824244656
contracourt: rename broadcastHeight to confirmHeight
The broadcastHeight was misleading because the commit resolver
is only created when the commitment transaction is confirmed.
2025-11-22 01:17:34 +01:00
Yong
8c8662c86a
Merge pull request #10384 from starius/rm-dead-code
lncli: remove dead code
2025-11-20 16:09:02 +08:00
Boris Nagaev
031c792b56
lncli: remove dead code
Variable chanPoints was accumulated but never used. Found with staticcheck tool.

The code is dead since 8f5d78c875
2025-11-19 21:51:40 -03:00
András Bánki-Horváth
194a9f7597
Merge pull request #10378 from ziggie1984/bugfix/graph-cache
graph: fix graph-cache issue
2025-11-19 18:01:31 +01:00
ziggie
4f40d45d06
docs: add release-notes for 20.1 2025-11-19 16:09:33 +01:00
ziggie
5225b9bbbc
graph: add regression test for the fixed behaviour 2025-11-19 16:09:33 +01:00
ziggie
575766c5f4
graph: fix graph cache population for channels with both policies disabled
Fix a bug where channels with both policies disabled were not added to
the graph cache during startup. When a policy update later re-enabled
one of the directions, the update would succeed in the database but fail
to update the graph cache (since the channel structure was never added),
preventing the channel from being used for routing.
2025-11-19 10:12:08 +01:00
Yong
31452a66a5
Merge pull request #10374 from ziggie1984/bugfix/fix-unit-test
Fix unit test for onion messages
2025-11-17 21:25:45 +08:00
ziggie
84504fa8c5
lnwire: implement SerializedSize for onion messages 2025-11-16 09:28:12 +01:00
Yong
841a29118a
Merge pull request #10310 from starius/bump-mapstructure
multi: update mapstructure/v2
2025-11-14 20:11:52 +08:00
Yong
ff20dd281e
Merge pull request #9432 from NishantBansal2003/close-addr-conf
multi: add upfront-shutdown-address to lnd.conf.
2025-11-14 15:54:58 +08:00
Yong
bb6e8f390b
Merge pull request #10366 from starius/mv-release-notes
docs: move release notes from 0.20.0 to 0.21.0
2025-11-14 15:53:04 +08:00
Boris Nagaev
86e3b1b05c
docs: move release notes from 0.20.0 to 0.21.0
Remove the chain notifier RPC note from the 0.20 release notes and add it to
0.21.
2025-11-13 12:54:25 -03:00
Yong
f6005ed350
Merge pull request #10352 from starius/chainnotifier-unavallable
chainrpc: return Unavailable while notifier starts
2025-11-13 23:27:52 +08:00
Nishant Bansal
36fb79ece5
docs: add release notes
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
2025-11-13 19:34:05 +05:30
Nishant Bansal
4c3cff14f2
itest: add itest for upfront-shutdown-address config
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
2025-11-13 19:32:51 +05:30
Nishant Bansal
e4c4d946fd
multi: add new config option upfront-shutdown-address
Introduced a new config value `upfront-shutdown-address`
in the `lnd.conf` file. This ensures that channel close
funds are transferred to the specified shutdown address.
The value applies to both the funder and the fundee but
can be overridden by the value specified during
`openchannel` or by the `channel acceptor`.

NOTE: If this field is set when opening a channel with a
peer that does not advertise support for upfront shutdown
feature, the channel open will fail.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
2025-11-13 19:32:51 +05:30
Olaoluwa Osuntokun
af6816b344
Merge pull request #10343 from lightningnetwork/0-21-0-staging
Merge branch `0-21-staging`
2025-11-12 16:34:20 -08:00
Elle Mouton
95b84a89b8
docs: update release notes 2025-11-12 22:54:06 +08:00
Elle Mouton
352b4d620a
graph/db: freeze sql migration queries 2025-11-12 22:54:06 +08:00
Elle Mouton
5db82a4233
go.mod: replace local sqldb 2025-11-12 22:54:06 +08:00
Elle Mouton
125325f625
multi: freeze graph SQL migration logic
Copy over all the code that the graph SQL migration needs to a
separate folder. This will let us advance the main graph SQL CRUD code
without worrying about changing the sql migration code. It will also let
us change the SQL queries without changing the migration. In this
commit, only the migration logic is "frozen" but in an upcoming commit,
the sqlc queries & models will be frozen too.
2025-11-12 22:54:05 +08:00
Elle Mouton
07d0f0842c
graph/db: remove TestPopulateViaMigration
This tests was a temporary helper to let devs test the graph SQL
migration before it was plugged in to LND. But that migration has now
shipped and so we can remove this.
2025-11-12 22:54:05 +08:00
Elle Mouton
33c3809e4b
graph/db: remove outdated comment 2025-11-12 22:54:05 +08:00
Elle Mouton
855e579c3f
graph: remove DB interface 2025-11-12 22:54:05 +08:00
Elle Mouton
edc021f711
graph/db: simplify auth proof and edge info
Remove various unused fields and methods.
2025-11-12 22:54:05 +08:00
Elle Mouton
80e70096cb
multi: add models.Node V1 constructor
Add a version field to models.Node and a V1 constructor for it.
2025-11-12 22:54:05 +08:00
Elle Mouton
8f205e2d1c
multi: remove HaveNodeAnnouncement field from Node
Remove the 2 sources of truth here. If we have a signature for the
node, then we have the announcement.
2025-11-12 22:54:04 +08:00
Elle Mouton
a394938dfd
models: simplify models.Node
Simplify the struct by removing un-used methods and outdated comments.
2025-11-12 22:54:04 +08:00
Gijs van Dam
93c50bc69a
docs: update release notes for 0.21.0 2025-11-12 22:54:04 +08:00
Gijs van Dam
c37b18cf82
chore: thread context through to SendCustomMessage
The only way to unblock SendCustomMessage is if the peer activates,
disconnects or the server shuts down. This means that if the context is
cancelled, we will still wait until one of those other events happen.

With this commit we thread the context through to SendCustomMessage, so
that if the context is cancelled, we can return early. This improves the
cancellation semantics.
2025-11-12 22:54:04 +08:00
Gijs van Dam
07dc74e198
multi: endpoints for onion messages
This commit creates the necessary endpoints for onion messages.
Specifically, it adds the following:

- `SendOnionMessage` endpoint to send onion messages.
- `SubscribeOnionMessages` endpoint to subscribe to incoming onion
  messages.

It uses the `msgmux` package to handle the onion messages.
2025-11-12 22:54:04 +08:00
Gijs van Dam
2ee0702942
lnrpc: SendOnionMessage rpc endpoint 2025-11-12 22:54:04 +08:00
Gijs van Dam
cb2d8abe40
lnwire: add onion message type
This message type is a message that carries an onion-encrypted payload
used for BOLT12 messages.
2025-11-12 22:54:03 +08:00
ziggie
c08f918ffc
paymentsdb: make verifyAttempt more robust
We now return an error when blinded and non blinded attempts are
combined. This was theoretically possible to register a legacy
attempt in combination with a blinded payment. This would have
been prevented by other checks in the code because legacy payments
are not split into shards.
2025-11-12 22:54:03 +08:00
ziggie
cbfc0e0dc2
mulit: don't set customData on the lnrpc route level
We now make sure we only set the custom channel data in the
lnrpc.Route only if it contains relevant data.
2025-11-12 22:54:03 +08:00
Elle Mouton
f35c295bd8
discovery: let reject cache use gossip version in key
Since the gossip protocols are completely disjoint, we need to treat
messages on the two protocols completely separately and should not let
rejections on one protocol affect how we treat messages on the other.
2025-11-12 22:54:03 +08:00
Elle Mouton
b57714b00c
graph/db: use lnwire.GossipVersion instead of ProtocolVersion 2025-11-12 22:54:03 +08:00
Elle Mouton
ef0e6b78bb
lnwire: let gossip messages implement GossipMessage 2025-11-12 22:54:03 +08:00
Elle Mouton
4d3bf6379a
lnwire: define GossipVersion and GossipMessage
Define a GossipVersion enum along with a GossipMessage interface to be
satisfied by all gossip related messages. This will be useful later on
when we want to make decisions based on the protocol version that a
message is part of.
2025-11-12 22:54:02 +08:00
Elle Mouton
70054ac402
lnwire: add NodeAnnouncement interface
And ensure that both versions 1 and 2 implement it.
2025-11-12 22:54:02 +08:00
ziggie
6cc7a88e99
makefile: add linter config check 2025-11-12 22:54:02 +08:00
ziggie
c324922a47
CI: remove unnecessary custom-gcl.yml file
The custom file is only needed in the tools directory.
2025-11-12 22:54:02 +08:00
ziggie
b6dd3a32f7
CI: also use go tool for gosimports 2025-11-12 22:54:02 +08:00
ziggie
d2d56f2968
linter: remove unsupported linters from the codebase 2025-11-12 22:54:02 +08:00
ziggie
13e7a6e782
tools: update golangci-lint to v2
The config file format changed. The tool golangci-lint migrate
was used to migrate the old config. However old comments and also
the structure of the disabled linters was preserved.

Moreover the new v2 version introduced new linters, we disable
3 of them because they are very noise and we do not really want
to check for them: funcorder, noinlineerr, embeddedstructfieldcheck.
2025-11-12 22:54:01 +08:00
ziggie
f9dccd3e20
tools: use new golang 1.24 tool directive 2025-11-12 22:54:01 +08:00
Yong
04917644ff
Merge pull request #10361 from Roasbeef/master-v-20-99
build: bump master version to v0.20.99
2025-11-12 22:52:04 +08:00
Yong
93ca7eda03
Merge pull request #10362 from Roasbeef/security-key-update-2025
security: update security@ PGP key
2025-11-12 22:37:12 +08:00
Olaoluwa Osuntokun
7e7bf59133
security: update security@ PGP key
The key was renewed, but the gist linked in the repo wasn't updated.
2025-11-11 14:44:50 -08:00
Olaoluwa Osuntokun
2cd17e3d2f
build: bump master version to v0.20.99
This reflects the fact that master is beyond the last major release and
will be a super set of anything in the v0.20.x series.
2025-11-11 14:41:48 -08:00
Olaoluwa Osuntokun
604dbdadd2
Merge pull request #10359 from Roasbeef/release-doc
docs: create new document describing proposed release/branch management
2025-11-11 14:39:58 -08:00
Olaoluwa Osuntokun
b9ea7070c2
Merge pull request #10358 from Roasbeef/v0.20.x-branch
build: bump version to v0.20.0
2025-11-11 14:36:48 -08:00
Olaoluwa Osuntokun
113aaf1607
docs: create new document describing proposed release/branch management
In this PR, we aim to create a central document that describes how we
want to drive the release process, as well as branch management going
forward.
2025-11-11 12:42:27 -08:00
Boris Nagaev
e4313eba85
docs: add release notes entry
Also fixed a typo in release notes.
2025-11-06 23:01:54 -03:00
Boris Nagaev
9520a390ed
chainrpc: return Unavailable while notifier starts
From v0.20.0-rc3 (commit c6f458e478) onward
the ChainNotifier sub-server may still be initialising when clients attempt
to subscribe, currently resulting in a gRPC Unknown error with a plain-text
message. Change the notifier RPC endpoints to return codes.Unavailable instead
so clients can reliably interpret the condition as "retry later" and handle
the startup lag without unstable string matching.
2025-11-06 21:58:46 -03:00
Boris Nagaev
6ee8e2a84d
multi: update mapstructure/v2
go-viper's mapstructure may leak sensitive information in logs when processing
malformed data. Updating to the version where the bug is fixed.
2025-10-21 15:22:45 -03:00
Olaoluwa Osuntokun
8266b96f2e
feature: start to set the require bit for channel_type
Related to this spec PR: https://github.com/lightning/bolts/pull/1232.

To start with, we'll start to set the required feature bit for the
`channel_type` feature where applicable.
2025-04-03 16:07:39 -07:00
1205 changed files with 145537 additions and 40304 deletions

View file

@ -0,0 +1,23 @@
---
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(./scripts/comment-on-duplicates.sh:*)
description: Find duplicate GitHub issues
---
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #2
4. Next, feed the results from #2 and #3 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, use the comment script to post duplicates:
```
./scripts/comment-on-duplicates.sh --base-issue <issue-number> --potential-duplicates <dup1> <dup2> <dup3>
```
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` and the comment script (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first

View file

@ -1,4 +0,0 @@
version: v1.57.0
plugins:
- module: 'github.com/lightningnetwork/lnd/tools/linters'
path: ./tools/linters

View file

@ -52,8 +52,8 @@ runs:
# The key is used to create and later look up the cache. It's made of
# four parts:
# - The base part is made from the OS name, Go version and a
# job-specified key prefix. Example: `linux-go-1.25.3-unit-test-`.
# It ensures that a job running on Linux with Go 1.25 only looks for
# job-specified key prefix. Example: `linux-go-1.26.4-unit-test-`.
# It ensures that a job running on Linux with Go 1.26 only looks for
# caches from the same environment.
# - The unique part is the `hashFiles('**/go.sum')`, which calculates a
# hash (a fingerprint) of the go.sum file.

124
.github/workflows/backport.yml vendored Normal file
View file

@ -0,0 +1,124 @@
name: Backport
on:
pull_request_target:
types: [closed, labeled]
permissions:
contents: write
pull-requests: write
issues: read
jobs:
backport:
name: Backport PR
runs-on: ubuntu-latest
# Only run on merged PRs with backport labels.
# Labels must match pattern: backport-v* (e.g., backport-v0.20.x-branch).
# This excludes labels like "backport candidate" or "backport-candidate".
if: |
github.event.pull_request.merged == true &&
contains(join(github.event.pull_request.labels.*.name, ','), 'backport-v')
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.base.ref }}
- name: Validate target branches exist
id: validate
shell: bash
run: |
# Extract all backport labels
labels='${{ toJSON(github.event.pull_request.labels.*.name) }}'
echo "All labels: $labels"
# Parse labels and extract branch names
# Only match labels starting with "backport-v" to exclude labels like
# "backport candidate" or "backport-candidate"
backport_labels=$(echo "$labels" | jq -r '.[] | select(startswith("backport-v"))')
if [ -z "$backport_labels" ]; then
echo "::error::No valid backport labels found (must start with 'backport-v')"
exit 1
fi
echo "Found backport labels:"
echo "$backport_labels"
# Check each target branch exists
missing_branches=()
valid_branches=()
while IFS= read -r label; do
# Extract branch name (everything after "backport-")
branch_name="${label#backport-}"
echo "Checking if branch exists: $branch_name"
# Check if branch exists in remote
if ! git ls-remote --heads origin "$branch_name" | grep -q "$branch_name"; then
echo "::warning::Target branch '$branch_name' does not exist (from label '$label')"
missing_branches+=("$branch_name")
else
echo "✓ Branch '$branch_name' exists"
valid_branches+=("$branch_name")
fi
done <<< "$backport_labels"
# Report validation results
if [ ${#missing_branches[@]} -gt 0 ]; then
echo "::warning::The following target branches do not exist and will be skipped: ${missing_branches[*]}"
echo "::warning::Please check the branch names or create the branches before retrying"
fi
# Only fail if ALL branches are invalid
if [ ${#valid_branches[@]} -eq 0 ]; then
echo "::error::No valid target branches found. All backport labels reference non-existent branches."
exit 1
fi
echo "✓ Found ${#valid_branches[@]} valid target branch(es): ${valid_branches[*]}"
if [ ${#missing_branches[@]} -gt 0 ]; then
echo "⚠ Skipping ${#missing_branches[@]} invalid branch(es): ${missing_branches[*]}"
fi
- name: Create backport PRs
# Uses version v3.4, we pin to a hash here. For more details to
# available versions, see:
# https://github.com/korthout/backport-action/releases.
uses: korthout/backport-action@d07416681cab29bf2661702f925f020aaa962997
with:
# Automatically detect target branches from labels.
# Labels must be in format: backport-v0.20.x-branch (must start
# with "backport-v"). This excludes labels like "backport candidate"
# or "backport-candidate". The pattern extracts everything after
# "backport-" as the branch name.
label_pattern: '^backport-(v.+)$'
# GitHub token for creating PRs.
github_token: ${{ secrets.GITHUB_TOKEN }}
# PR title format - shows it's a backport with original PR number.
pull_title: '[${target_branch}] Backport #${pull_number}: ${pull_title}'
# PR description template - links back to original PR.
pull_description: |-
Backport of #${pull_number}
---
${pull_description}
# Automatically add labels to backport PRs.
# The 'no-changelog' label skips the release notes check in CI.
add_labels: no-changelog
# Copy milestone from original PR to backport PR.
copy_milestone: true
# Merge strategy - skip merge commits, use cherry-pick only.
merge_commits: skip
# If conflicts occur, create a draft PR with conflict markers.
experimental: '{"conflict_resolution": "draft_commit_conflicts"}'

View file

@ -0,0 +1,176 @@
name: Claude Issue Dedupe
description: Automatically dedupe GitHub issues using Claude Code
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process for duplicate detection'
required: true
type: string
# Default to read-only. The find-duplicates job reads untrusted issue text with
# the model, so it must not hold a write token; post-comment takes issues: write
# but runs no model and only shells out to scripts/comment-on-duplicates.sh,
# which re-validates every issue number it is handed.
permissions:
contents: read
# Serialize runs for the same issue so an `issues: opened` event and a
# workflow_dispatch for the same number can't both read "no prior comment" and
# double-post. Mirrors pr-severity.yml.
concurrency:
group: claude-dedupe-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: true
jobs:
find-duplicates:
runs-on: ubuntu-latest
timeout-minutes: 10
# Read-only: the model inspects the issue and searches for duplicates, then
# records the candidate issue numbers to a file.
permissions:
contents: read
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Find duplicate issues with Claude
# Pinned to a full commit SHA rather than the mutable @v1 tag: this step
# feeds untrusted issue text to the model with CLAUDE_CODE_OAUTH_TOKEN in
# process, so a repointed tag would run attacker-controlled action code
# with that secret present. Bump deliberately when updating.
uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Accept any issue author: this job holds only read scope and merely
# records candidate issue numbers to a file; the comment is posted by
# a separate, model-free job. "*" is safe ONLY while this job stays
# read-only. Before granting this job a write token or a mutating tool
# (a write-capable gh subcommand, a Bash mutation), replace "*" with
# an explicit allowlist — otherwise any fork author's issue text would
# steer a privileged model.
allowed_non_write_users: "*"
model: claude-haiku-4-5-20251001
# Read-only gh tools plus Write to record the result. No comment or
# edit tools, and no access to the duplicate-comment script.
claude_args: >-
--allowedTools
"Bash(gh issue view:*)"
"Bash(gh search:*)"
"Bash(gh issue list:*)"
"Write"
prompt: |
Find up to 3 likely duplicate issues for issue
#${{ github.event.issue.number || inputs.issue_number }} in the
${{ github.repository }} repository. Follow these steps precisely:
1. View the issue and check whether it (a) is closed, (b) does not
need deduping (e.g. broad product feedback without a specific
solution, or positive feedback), or (c) already has a duplicates
comment. If any of these hold, write an empty `duplicates.txt`
(create the file with no content) and stop.
2. Summarize the issue.
3. Search GitHub for duplicates of this issue using several diverse
keyword searches and search approaches, based on the summary.
4. Filter out false positives that are likely not actually
duplicates of the original issue. If no plausible duplicates
remain, write an empty `duplicates.txt` and stop.
5. Otherwise, write the chosen duplicate issue numbers to a file
named `duplicates.txt` in the current working directory: digits
only, one issue number per line, at most 3 lines. Do not include
`#`, URLs, or any other text.
Notes:
- Use `gh` to interact with GitHub, not web fetch.
- Do NOT use any tools beyond `gh issue view`, `gh search`,
`gh issue list`, and `Write`. You do NOT post comments; a separate
step does that from the file you write.
- Make a todo list first.
- name: Upload duplicate candidates
uses: actions/upload-artifact@v4
with:
name: dedupe-result
path: duplicates.txt
if-no-files-found: warn
retention-days: 1
post-comment:
runs-on: ubuntu-latest
needs: find-duplicates
timeout-minutes: 5
# Write scope lives here, in a job that runs no model. The base issue number
# comes from the trusted event payload, and comment-on-duplicates.sh
# re-validates every candidate issue number before posting.
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Download duplicate candidates
uses: actions/download-artifact@v4
# find-duplicates uploads with if-no-files-found: warn, so when the
# model writes no file at all (timeout, refusal) no artifact exists and
# download-artifact would otherwise hard-fail the job. Tolerate a
# missing artifact so the no-op guard in the next step is reachable.
continue-on-error: true
with:
name: dedupe-result
path: result
- name: Post duplicate comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
BASE_ISSUE: ${{ github.event.issue.number || inputs.issue_number }}
run: |
set -euo pipefail
# Distinguish a genuine "no duplicates" verdict from a find-duplicates
# run that produced no artifact at all (model crash/timeout, or a
# tolerated missing-artifact download). The latter gets a warning so a
# broken run doesn't read as a healthy no-op, mirroring the pr-severity
# apply step; the present-but-empty case stays a silent no-op.
if [[ ! -f result/duplicates.txt ]]; then
echo "::warning::dedupe find-duplicates produced no result; nothing posted."
exit 0
fi
if [[ ! -s result/duplicates.txt ]]; then
echo "No duplicate candidates; nothing to post."
exit 0
fi
# Extract up to 3 purely-numeric issue ids. comment-on-duplicates.sh
# re-validates these and the base issue (numeric, existing, at most 3)
# before posting.
mapfile -t DUPS < <(grep -oE '^[0-9]+$' result/duplicates.txt | head -n 3)
if [[ ${#DUPS[@]} -eq 0 ]]; then
echo "No valid numeric duplicate ids; nothing to post."
exit 0
fi
./scripts/comment-on-duplicates.sh \
--base-issue "$BASE_ISSUE" \
--potential-duplicates "${DUPS[@]}"

62
.github/workflows/claude.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Checkout PR branch (handles fork PRs)
if: github.event.issue.pull_request || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "issue_comment" ]; then
PR_NUMBER=${{ github.event.issue.number }}
else
PR_NUMBER=${{ github.event.pull_request.number }}
fi
gh pr checkout "$PR_NUMBER"
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

66
.github/workflows/gateway.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: gateway
# Opt-in code-review bot. Triggered by a `/gateway <command>` comment on a PR
# (e.g. `/gateway review`); review/approve commands are gated to maintainers.
# Comment-commands only — no pull_request triggers — so fork PRs (which receive
# no secrets) never spawn failing runs. v0.5.0 added the
# pull_request_review_comment trigger: /gateway dismiss, promote, and explain
# now also work as replies on a finding's inline thread (finding id inferred
# from the thread when omitted). Also a comment event — same fork-PR safety
# profile as issue_comment.
#
# Thin shim: the public lightninglabs/gateway-action mints an App token and
# checks out the private gateway runtime at execution time. The runtime stays
# private; only this entry point is public.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
# The action mints an App installation token internally; the GITHUB_TOKEN
# handed to this shim is unused, so we minimise it.
contents: read
jobs:
review:
# issue_comment fires for all issues and every PR comment. Filter to PR
# comments that look like a /gateway command so unrelated comments don't
# spin up a no-op runner. `contains` (not `startsWith`) because the runtime
# accepts the command at column 0 of any line, including multi-line bodies.
if: >-
${{
(github.event_name == 'issue_comment'
&& github.event.issue.pull_request != null
&& contains(github.event.comment.body, '/gateway')) ||
(github.event_name == 'pull_request_review_comment'
&& contains(github.event.comment.body, '/gateway'))
}}
runs-on: ubuntu-latest
timeout-minutes: 15
env:
GATEWAY_REVIEW_MODE: multi
steps:
- uses: lightninglabs/gateway-action@334a8455ee316e40668ae3ac85249150c62704ec # v0.6.0
with:
# Pin the private runtime to an immutable commit (matches the action
# SHA-pin above) so runtime upgrades go through an lnd PR, not a moved
# tag. Without this, runtime_ref defaults to the v0.6.0 tag.
runtime_ref: 75f6e67deac362bdcfc10d10629ddcf69c0e2615 # gateway v0.6.0
event_name: ${{ github.event_name }}
event_action: ${{ github.event.action }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.issue.number || github.event.pull_request.number }}
actor: ${{ github.event.sender.login }}
comment_body: ${{ github.event.comment.body }}
comment_id: ${{ github.event.comment.id }}
comment_in_reply_to: ${{ github.event.comment.in_reply_to_id }}
# installation_id intentionally omitted: as of gateway v0.4.4 the
# runtime resolves the App installation covering this repo from
# app_id/private_key, so a hardcoded (and easily wrong-org) id is no
# longer needed.
app_id: ${{ secrets.GATEWAY_APP_ID }}
private_key: ${{ secrets.GATEWAY_PRIVATE_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

109
.github/workflows/govulncheck.yml vendored Normal file
View file

@ -0,0 +1,109 @@
name: Vulnerability scan
on:
workflow_dispatch:
schedule:
# Run weekly to catch newly published vulnerabilities even when the code
# does not change.
- cron: "0 9 * * 1"
pull_request:
paths:
- ".github/workflows/govulncheck.yml"
- ".github/actions/setup-go/action.yml"
- "Makefile"
- "make/release_flags.mk"
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
push:
branches:
- "master"
paths:
- ".github/workflows/govulncheck.yml"
- ".github/actions/setup-go/action.yml"
- "Makefile"
- "make/release_flags.mk"
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
merge_group:
branches:
- "master"
permissions:
contents: read
defaults:
run:
shell: bash
env:
# If you change this please also update GO_VERSION in Makefile (then run
# `make lint` to see where else it needs to be updated as well).
GO_VERSION: 1.26.4
jobs:
govulncheck:
name: Scan release binaries
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Go ${{ env.GO_VERSION }}
uses: ./.github/actions/setup-go
with:
go-version: '${{ env.GO_VERSION }}'
key-prefix: govulncheck
use-build-cache: 'no'
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.3.0
- name: Build release binaries
run: make release-install
- name: Run govulncheck
run: |
set +e
gopath="$(go env GOPATH)"
final_exit_code=0
advisory_findings=0
for binary in lnd lncli; do
output="govulncheck-${binary}.txt"
"${gopath}/bin/govulncheck" \
-mode=binary \
"${gopath}/bin/${binary}" 2>&1 | tee "${output}"
exit_code=${PIPESTATUS[0]}
{
echo "### govulncheck ${binary}"
echo
echo '```'
sed -n '1,200p' "${output}"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
if [ "$exit_code" -eq 3 ]; then
advisory_findings=1
continue
fi
if [ "$exit_code" -ne 0 ] && [ "$final_exit_code" -eq 0 ]; then
final_exit_code="$exit_code"
fi
done
if [ "$advisory_findings" -eq 1 ]; then
echo "::warning title=govulncheck findings::govulncheck found vulnerabilities; see the job summary for details."
{
echo
echo "> govulncheck exited with code 3 for one or more release binaries. This job is advisory while the existing vulnerability baseline is remediated."
} >> "$GITHUB_STEP_SUMMARY"
fi
exit "$final_exit_code"

View file

@ -27,7 +27,8 @@ defaults:
shell: bash
env:
BITCOIN_VERSION: "29"
# Accepts either a major image tag like "30" or a patch tag like "29.1".
BITCOIN_VERSION: "31"
# TRANCHES defines the number of tranches used in the itests.
TRANCHES: 16
@ -40,7 +41,7 @@ env:
# If you change this please also update GO_VERSION in Makefile (then run
# `make lint` to see where else it needs to be updated as well).
GO_VERSION: 1.25.3
GO_VERSION: 1.26.4
jobs:
static-checks:
@ -176,7 +177,7 @@ jobs:
- name: amd64
sys: darwin-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 windows-amd64
- name: arm
sys: darwin-arm64 freebsd-arm linux-armv6 linux-armv7 linux-arm64 windows-arm
sys: darwin-arm64 freebsd-arm linux-armv6 linux-armv7 linux-arm64 windows-arm64
steps:
- name: Git checkout
uses: actions/checkout@v5
@ -212,6 +213,8 @@ jobs:
- unit tags="test_db_sqlite"
- unit tags="test_db_postgres"
- unit-race
- unit-race tags="test_db_sqlite"
- unit-race tags="test_db_postgres"
- unit-module
steps:
@ -278,6 +281,8 @@ jobs:
args: backend=btcd cover=1
- name: bitcoind
args: backend=bitcoind cover=1
- name: bitcoind-miner
args: backend=bitcoind minerbackend=bitcoind cover=1
- name: bitcoind-notxindex
args: backend="bitcoind notxindex"
- name: neutrino
@ -546,7 +551,7 @@ jobs:
fail-fast: false
matrix:
pinned_dep:
- google.golang.org/grpc v1.59.0
- google.golang.org/grpc v1.79.3
- github.com/golang/protobuf v1.5.4
steps:
@ -600,6 +605,14 @@ jobs:
- name: 🛡️ Backwards compatibility test
run: make backwards-compat-test
- name: 📋 Upload node logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: bw-compat-logs
path: scripts/bw-compatibility-test/logs/
retention-days: 7
#########################################
# Auto Cache Cleanup on Pull Requests
#########################################

313
.github/workflows/pr-severity.yml vendored Normal file
View file

@ -0,0 +1,313 @@
name: PR Severity Classification
on:
# Use pull_request_target so the workflow runs on fork PRs with the base
# repository's workflow definition. The classify job below reads PR metadata
# with a read-only token and never checks out or executes PR code; the write
# scope needed to apply the label lives in a separate, model-free job.
pull_request_target:
types: [opened, synchronize, labeled]
# Default the whole workflow to read-only. Each job opts into exactly the scope
# it needs: classify stays read-only (untrusted PR metadata reaches the model,
# so it must not hold a write token), apply takes pull-requests: write but runs
# no model.
permissions:
contents: read
concurrency:
group: pr-severity-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
classify:
name: Classify PR Severity
runs-on: ubuntu-latest
# Cap the model run: it fires on every pull_request_target synchronize with
# attacker-controllable input, so bound its runner-minute/token cost rather
# than inheriting GitHub's 6h default. Mirrors the dedupe workflow.
timeout-minutes: 10
# Read-only: the classifier only inspects PR metadata via the GitHub API.
permissions:
contents: read
pull-requests: read
# Skip if PR has skip-severity-check label.
# For labeled events, only run if 'reclassify' label was added.
if: |
!contains(github.event.pull_request.labels.*.name, 'skip-severity-check') &&
(github.event.action != 'labeled' || github.event.label.name == 'reclassify')
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
# Don't leave the job token in .git/config: nothing here needs a
# persisted git credential, and the classifier runs on untrusted
# fork-PR input.
persist-credentials: false
- name: Classify PR with Claude
# Pinned to a full commit SHA rather than the mutable @v1 tag: this step
# runs on pull_request_target with CLAUDE_CODE_OAUTH_TOKEN and
# GITHUB_TOKEN in-process and is reachable by any fork author, so a
# repointed tag would run attacker-controlled action code with those
# secrets present. Bump deliberately when updating.
uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1
env:
# gh (invoked by the Bash(gh pr view) tool) authenticates from
# GH_TOKEN; set it explicitly so classification doesn't depend on the
# action propagating its github_token input into the tool environment,
# matching the dedupe find-duplicates step.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# Accept any PR author: this job holds only a read-only token, reads
# PR metadata via the API, and writes its verdict to a file. The
# privileged label/comment step runs separately without the model.
# "*" is safe ONLY while this job stays read-only. Before granting it
# a write token or a mutating tool (a write-capable gh subcommand, a
# Bash mutation), replace "*" with an explicit allowlist — otherwise
# any fork author's PR text would steer a privileged model.
allowed_non_write_users: "*"
# Pin the model so the severity decision (which gates the label the
# apply job applies) is reproducible from the workflow file and does
# not drift on an action-default change.
model: claude-sonnet-5
# The classifier only needs to read PR data and record its verdict to
# the workspace. It has no write-capable gh tools.
#
# Security note: because this runs on pull_request_target, the OAuth
# token above is present in-process while the model reads untrusted
# fork-PR text. The read-only github_token bounds what the model can
# WRITE via the API, but the in-process token's protection rests on
# this allowlist staying minimal — read-only `gh pr view` plus
# `Write`, with no network- or shell-mutating tool a prompt injection
# could use to exfiltrate it. Keep it that way: do not add `Bash`
# verbs beyond `gh pr view`, and never add a tool that can make
# outbound requests. (A stricter design would drop Bash entirely and
# pre-fetch PR metadata via the API in a separate step.)
claude_args: --allowedTools "Bash(gh pr view:*)" "Write"
prompt: |
You are a PR severity classifier for the lnd (Lightning Network Daemon) repository.
## Tool Constraints
You ONLY have access to:
- `gh pr view` - to read PR metadata
- `Write` - to record your verdict to files
You do NOT have access to `gh api`, `gh label`, `gh pr edit`,
`gh pr comment`, or any other command. Do not attempt to use them.
You do NOT apply labels or post comments yourself. A separate,
deterministic step reads the files you write and applies the label
and comment. Your job is only to classify and record the result.
## Your Task
Analyze PR #${{ github.event.pull_request.number }} and:
1. Determine its severity level based on the files changed
2. Record the severity, whether a comment should be posted, and the
comment body, to files (see "Output" below).
## Severity Levels
**CRITICAL** (severity-critical) - Requires expert review:
- lnwallet/* - Wallet operations, channel funding, signing, commitment transactions
- htlcswitch/* - HTLC forwarding, payment routing state machine
- contractcourt/* - On-chain dispute resolution, breach handling
- sweep/* - Output sweeping, fund recovery, fee bumping
- peer/*, brontide/* - Encrypted peer connections, Noise protocol
- keychain/* - Private key derivation and management
- input/* - Script signing, witness generation, MuSig2
- channeldb/* - Channel state persistence, database migrations
- funding/* - Channel funding workflow coordination
- lnwire/* - Lightning wire protocol messages
- server.go, rpcserver.go - Core server coordination
**HIGH** (severity-high) - Requires knowledgeable engineer:
- routing/* - Payment pathfinding algorithms
- invoices/* - Invoice management and settlement
- discovery/* - Gossip protocol
- graph/* - Network graph maintenance
- watchtower/* - Breach remediation
- feature/* - Feature bit management
- lnrpc/* - RPC/API definitions
- macaroons/*, walletunlocker/*, cert/* - Auth/security
- chainntnfs/*, chanacceptor/*, protofsm/*, sqldb/*
**MEDIUM** (severity-medium) - Focused review:
- cmd/* - CLI client commands (do NOT inherit severity from server-side packages with similar names)
- payments/*, autopilot/*, lncfg/*, chanfitness/*
- netann/*, kvdb/*, chanbackup/*, aezeed/*, tor/*
- zpay32/*, tlv/*, fn/*, record/*, amp/*
- *.proto files (API changes)
- Other Go files not categorized above
**LOW** (severity-low) - Best-effort review:
- docs/*, release-notes/*, *.md files
- scripts/*, tools/*, contrib/*, make/*, docker/*
- itest/*, lntest/*, *_test.go (test-only changes)
- .github/* (CI/CD configuration)
## Classification Rules
1. The HIGHEST severity file determines the PR severity
2. Classify files by their actual package path, NOT by filename keywords.
Files under cmd/* are CLI client code and should always be MEDIUM,
even if the filename contains a server-side package name (e.g.
cmd/commands/cmd_walletunlocker.go is MEDIUM, not HIGH).
3. Bump severity UP one level if:
- PR touches >20 files (excluding tests and auto-generated files)
- PR has >500 lines changed (excluding tests and auto-generated files)
- PR touches multiple distinct critical packages
4. Check for override labels first (severity-override-*). If present, respect the override.
5. Database migrations (channeldb/migration*, sqldb/*, wtdb/*) are always CRITICAL
## Files to Exclude from Line/File Counting
When calculating file count and lines changed for severity bumps, exclude:
- Test files: *_test.go, itest/*, lntest/*
- Auto-generated files: *.pb.go, *.pb.gw.go, *.pb.json.go, *.sql.go, *_generated.go
- Mock files: mock_*.go, *_mock.go
## Steps
1. Read the current labels AND comments to detect overrides and prior
bot activity:
```
gh pr view ${{ github.event.pull_request.number }} --json labels,comments
```
Note which `severity-*` label (if any) is currently applied. This
is the "previous severity". Look for the HTML marker
`<!-- pr-severity-bot -->` in comment bodies to tell whether the
bot has commented before.
2. If an override label exists (severity-override-*), use that level
and skip classification.
3. Get the list of changed files:
```
gh pr view ${{ github.event.pull_request.number }} --json files,additions,deletions
```
4. Classify each file and determine the new overall severity.
5. **Decide whether a comment should be posted.** Set should_comment
to "true" only if EITHER:
- The bot has NOT commented before (no existing comment with
`<!-- pr-severity-bot -->`), OR
- The newly determined severity is DIFFERENT from the previous
severity label.
Otherwise set should_comment to "false" (the label may still be
updated by the apply step, but no new comment is posted).
## Output
Record your verdict by writing these files in the current working
directory (the repository root). Do NOT apply labels or comment
yourself.
1. `severity.txt` - exactly one lowercase word, one of:
`critical`, `high`, `medium`, `low`. Nothing else.
2. `should_comment.txt` - exactly `true` or `false`.
3. `comment.md` - only if should_comment is `true`. The full comment
markdown, in this format:
If this is a severity CHANGE (previous label existed but differs),
prepend: `> ⚠️ Severity changed: **<OLD>** → **<NEW>** (files changed since last classification)`
```markdown
## <emoji> PR Severity: **<LEVEL>**
> <source> | <N> files | <M> lines changed
<details>
<summary>🔴 <strong>Critical</strong> (N files)</summary>
- `path/to/file1.go` - reason
- `path/to/file2.go` - reason
</details>
[repeat for other tiers if applicable]
### Analysis
<Your explanation of why this severity was chosen, any concerns, etc.>
---
<sub>To override, add a `severity-override-{critical,high,medium,low}` label.</sub>
<!-- pr-severity-bot -->
```
If should_comment is `false`, do not create `comment.md`.
Keep the comment concise and factual: it is posted verbatim under
the bot's identity, and the apply step defangs any `@`-mentions
and links, so do not rely on them.
## Emoji Mapping
- critical: 🔴
- high: 🟠
- medium: 🟡
- low: 🟢
- name: Upload classification result
uses: actions/upload-artifact@v4
with:
name: pr-severity-result
path: |
severity.txt
should_comment.txt
comment.md
if-no-files-found: warn
retention-days: 1
apply:
name: Apply Severity Label
runs-on: ubuntu-latest
needs: classify
timeout-minutes: 5
# Write scope lives here, in a job that runs no model. The only inputs are
# the PR number from the trusted event payload and the classifier's files,
# which are strictly validated before use.
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Needed for scripts/apply-pr-severity.sh; no persisted git credential
# is required here.
persist-credentials: false
- name: Download classification result
uses: actions/download-artifact@v4
# classify uploads with if-no-files-found: warn, so if the model writes
# no verdict at all (timeout, refusal) no artifact exists and
# download-artifact would otherwise hard-fail the job. Tolerate a
# missing artifact so the no-op guard in the next step is reachable.
continue-on-error: true
with:
name: pr-severity-result
path: result
- name: Apply label and comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
# Validate the severity, reconcile the label, and sanitize + post the
# model-authored comment. The logic lives in a checked-in script so the
# untrusted-comment sanitizer is unit-tested
# (scripts/apply-pr-severity_test.sh), mirroring how the dedupe workflow
# delegates to scripts/comment-on-duplicates.sh.
run: ./scripts/apply-pr-severity.sh result

View file

@ -12,7 +12,7 @@ defaults:
env:
# If you change this please also update GO_VERSION in Makefile (then run
# `make lint` to see where else it needs to be updated as well).
GO_VERSION: 1.25.3
GO_VERSION: 1.26.4
jobs:
########################
@ -40,7 +40,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: build release for all architectures
run: SKIP_VERSION_CHECK=1 make release tag=${{ env.RELEASE_VERSION }}
run: make release tag=${{ env.RELEASE_VERSION }}
- name: Create Release
uses: lightninglabs/gh-actions/action-gh-release@c7149b6a7818d1c39b36b69e727569897b6f2c5a
@ -81,10 +81,11 @@ jobs:
## Verifying the Release Timestamp
From this new version onwards, in addition time-stamping the _git tag_ with [OpenTimestamps](https://opentimestamps.org/), we'll also now timestamp the manifest file along with its signature. Two new files are now included along with the rest of our release artifacts: ` manifest-roasbeef-${{ env.RELEASE_VERSION }}.txt.asc.ots`.
From this new version onwards, in addition to time-stamping the _git tag_ with [OpenTimestamps](https://opentimestamps.org/), we'll also now timestamp the manifest file along with the `roasbeef` release signature. For final releases, and for release candidates when these optional artifacts are uploaded, timestamp proof files are included along with the rest of our release artifacts: `manifest-${{ env.RELEASE_VERSION }}.txt.ots` and `manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig.ots`.
Assuming you have the opentimestamps client installed locally, the timestamps can be verified with the following commands:
```
ots verify manifest-${{ env.RELEASE_VERSION }}.txt.ots -f manifest-${{ env.RELEASE_VERSION }}.txt
ots verify manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig.ots -f manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig
```

74
.github/workflows/verify-release.yaml vendored Normal file
View file

@ -0,0 +1,74 @@
name: Verify release
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Release version tag (e.g. v0.20.1-beta)'
required: true
permissions:
contents: write
jobs:
verify-release:
name: Verify release signatures and binaries
runs-on: ubuntu-latest
steps:
- name: git checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.version || github.sha }}
- name: Check final release OpenTimestamps asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version || github.event.release.tag_name }}
run: |
set -euo pipefail
if [[ "${VERSION}" =~ \.rc[0-9]+$ ]]; then
echo "Release candidate ${VERSION}; skipping OpenTimestamps asset check."
exit 0
fi
REQUIRED_MANIFEST_OTS="manifest-${VERSION}.txt.ots"
REQUIRED_SIG="manifest-roasbeef-${VERSION}.sig"
REQUIRED_SIG_OTS="${REQUIRED_SIG}.ots"
ASSETS="$(gh release view "${VERSION}" \
--repo "${{ github.repository }}" \
--json assets \
--jq '.assets[].name')"
for asset in "${REQUIRED_MANIFEST_OTS}" "${REQUIRED_SIG}" "${REQUIRED_SIG_OTS}"; do
if ! grep -Fxq "${asset}" <<< "${ASSETS}"; then
echo "ERROR: Final release ${VERSION} is missing ${asset}."
exit 1
fi
done
echo "Found required release timestamp artifacts:"
echo " ${REQUIRED_MANIFEST_OTS}"
echo " ${REQUIRED_SIG}"
echo " ${REQUIRED_SIG_OTS}"
- name: Verify release
env:
VERSION: ${{ inputs.version || github.event.release.tag_name }}
run: |
docker run --rm --entrypoint="" \
lightninglabs/lnd:${VERSION} \
/verify-install.sh ${VERSION}
- name: Set release back to draft on failure
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version || github.event.release.tag_name }}
run: |
gh release edit ${VERSION} \
--repo ${{ github.repository }} \
--draft

4
.gitignore vendored
View file

@ -40,6 +40,9 @@ itest/btcd-itest
itest/.logs-*
itest/cover
# Local lntest miner logs (dev artifacts)
lntest/miner/*.log
cmd/cmd
*.key
*.hex
@ -80,6 +83,7 @@ coverage.txt
# Release build directory (to avoid build.vcs.modified Golang build tag to be
# set to true by having untracked files in the working directory).
/lnd-*/
/.worktrees/
.aider*

View file

@ -1,7 +1,9 @@
version: "2"
run:
# If you change this please also update GO_VERSION in Makefile (then run
# `make lint` to see where else it needs to be updated as well).
go: "1.25.3"
go: "1.26.4"
# Abort after 10 minutes.
timeout: 10m
@ -21,95 +23,8 @@ run:
- kvdb_sqlite
- integration
linters-settings:
custom:
ll:
type: "module"
description: "Custom lll linter with 'S' log line exclusion."
settings:
# Max line length, lines longer will be reported.
line-length: 80
# Tab width in spaces.
tab-width: 8
# The regex that we will use to detect the start of an `S` log line.
log-regex: "^\\s*.*(L|l)og\\.(Info|Debug|Trace|Warn|Error|Critical)S\\("
errorlint:
# Check for incorrect fmt.Errorf error wrapping.
errorf: true
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
tagliatelle:
case:
rules:
json: snake
whitespace:
multi-func: true
multi-if: true
gosec:
excludes:
- G402 # Look for bad TLS connection settings.
- G306 # Poor file permissions used when writing to a new file.
- G601 # Implicit memory aliasing in for loop.
- G115 # Integer overflow in conversion.
staticcheck:
checks: ["-SA1019"]
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
lines: 200
# Checks the number of statements in a function.
statements: 80
dupl:
# Tokens count to trigger issue.
threshold: 200
nestif:
# Minimal complexity of if statements to report.
min-complexity: 10
nlreturn:
# Size of the block (including return statement that is still "OK")
# so no return split required.
block-size: 3
gomnd:
# List of numbers to exclude from analysis.
# The numbers should be written as string.
# Values always ignored: "1", "1.0", "0" and "0.0"
# Default: []
ignored-numbers:
- '0666'
- '0755'
# List of function patterns to exclude from analysis.
# Values always ignored: `time.Date`
# Default: []
ignored-functions:
- 'math.*'
- 'strconv.ParseInt'
- 'errors.Wrap'
gomoddirectives:
replace-local: true
replace-allow-list:
# See go.mod for the explanation why these are needed.
- github.com/ulikunitz/xz
- github.com/gogo/protobuf
- google.golang.org/protobuf
- github.com/lightningnetwork/lnd/sqldb
linters:
enable-all: true
default: all
disable:
# We instead use our own custom line length linter called `ll` since
# then we can ignore log lines.
@ -131,25 +46,33 @@ linters:
# Init functions are used by loggers throughout the codebase.
- gochecknoinits
# Deprecated linters. See https://golangci-lint.run/usage/linters/.
- bodyclose
# contextcheck requires threading context.Context through many existing
# function signatures (including test harnesses), so we leave it off for
# now.
- contextcheck
- nilerr
- noctx
- rowserrcheck
- sqlclosecheck
# tparallel requires adding t.Parallel() to a large number of existing
# subtests, which can surface shared-state races. Disabled until we can
# address it carefully.
- tparallel
# unparam has a sizeable backlog of unused parameters to clean up before it
# can be enabled.
- unparam
- wastedassign
# Disable gofumpt as it has weird behavior regarding formatting multiple
# lines for a function which is in conflict with our contribution
# guidelines. See https://github.com/mvdan/gofumpt/issues/235.
- gofumpt
# nilerr is too noisy for our code base: most reports are intentional error
# swallowing (documented with comments) or false positives where a boolean
# check is mistaken for an error check.
- nilerr
# Disable whitespace linter as it has conflict rules against our
# noctx would only flag a couple of interface methods and a test helper that
# have no context to thread through, so it adds little value for now.
- noctx
# Disable whitespace linters as it has conflict rules against our
# contribution guidelines.
- wsl
- wsl_v5
# Allow using default empty values.
- exhaustruct
@ -203,7 +126,6 @@ linters:
- testifylint
- perfsprint
- inamedparam
- copyloopvar
- tagalign
- protogetter
- revive
@ -211,88 +133,216 @@ linters:
- gosmopolitan
- intrange
- goconst
# Disable function order linter because we structure exported and unexported
# functions differently.
- funcorder
# Disable noinlineerr linter because we use it to inline errors.
- noinlineerr
# Disable embeddedstructfieldcheck linter because we use it to align
# structs. Because sometimes we have atomic fields that need to be aligned
# with means we need to assure that the field is at the beginning of the
# struct.
- embeddedstructfieldcheck
# Deprecated linters that have been replaced by newer ones.
- tenv
settings:
dupl:
# Tokens count to trigger issue.
threshold: 200
errorlint:
# Check for incorrect fmt.Errorf error wrapping.
errorf: true
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
lines: 200
# Checks the number of statements in a function.
statements: 80
gomoddirectives:
# See project's go.mod for the explanation why these are needed.
replace-allow-list:
- github.com/ulikunitz/xz
- github.com/gogo/protobuf
- google.golang.org/protobuf
- github.com/lightningnetwork/lnd/sqldb
- github.com/lightningnetwork/lightning-onion
replace-local: true
gosec:
excludes:
- G402 # Look for bad TLS connection settings.
- G306 # Poor file permissions used when writing to a new file.
- G601 # Implicit memory aliasing in for loop.
- G115 # Integer overflow in conversion.
nestif:
# Minimal complexity of if statements to report.
min-complexity: 10
nlreturn:
# Size of the block (including return statement that is still "OK")
# so no return split required.
block-size: 3
staticcheck:
checks:
- -SA1019
tagliatelle:
case:
rules:
json: snake
usetesting:
context-background: true
whitespace:
multi-if: true
multi-func: true
custom:
ll:
type: module
description: Custom lll linter with 'S' log line exclusion.
settings:
# Max line length, lines longer will be reported.
line-length: 80
# The regex that we will use to detect the start of an `S` log line.
log-regex: ^\s*.*(L|l)og\.(Info|Debug|Trace|Warn|Error|Critical)S\(
# Tab width in spaces.
tab-width: 8
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
# Allow duplications in tests so it's easier to follow a single unit
- dupl
- funlen
- gosec
- revive
# Exclude gosec from running for tests so that tests with weak
# randomness (math/rand) will pass the linter.
path: _test\.go
- linters:
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
- revive
path: mock*
- linters:
- funlen
- gosec
path: test*
# Allow duplicated code and fmt.Printf() in DB migrations.
- linters:
- dupl
- forbidigo
- godot
path: channeldb/migration*
# Allow duplicated code and fmt.Printf() in DB migration tests.
- linters:
- dupl
- forbidigo
- godot
path: channeldb/migtest
# Allow fmt.Printf() in commands.
- linters:
- forbidigo
path: cmd/commands/*
# Allow fmt.Printf() in config parsing.
- linters:
- forbidigo
path: config\.go
- linters:
- forbidigo
path: lnd\.go
- linters:
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
path: lnmock/*
- linters:
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
path: mock*
# Skip autogenerated files for mobile and gRPC as well as copied code for
# internal use.
paths:
- third_party$
- builtin$
- examples$
- "mobile\\/.*generated\\.go"
- "\\.pb\\.go$"
- "\\.pb\\.gw\\.go$"
- "internal\\/musig2v040"
- channeldb/migration_01_to_11
- channeldb/migration/lnwire21
- payments/db/migration1/lnwire
- payments/db/migration1/record
issues:
# Only show newly introduced problems.
new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
# Skip autogenerated files for mobile and gRPC as well as copied code for
# internal use.
skip-files:
- "mobile\\/.*generated\\.go"
- "\\.pb\\.go$"
- "\\.pb\\.gw\\.go$"
- "internal\\/musig2v040"
formatters:
enable:
- gci
- gofmt
- goimports
skip-dirs:
- channeldb/migration_01_to_11
- channeldb/migration/lnwire21
settings:
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
exclude-rules:
# Exclude gosec from running for tests so that tests with weak randomness
# (math/rand) will pass the linter.
- path: _test\.go
linters:
- gosec
- funlen
- revive
# Allow duplications in tests so it's easier to follow a single unit
# test.
- dupl
- path: mock*
linters:
- revive
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
- path: test*
linters:
- gosec
- funlen
# Allow duplicated code and fmt.Printf() in DB migrations.
- path: channeldb/migration*
linters:
- dupl
- forbidigo
- godot
# Allow duplicated code and fmt.Printf() in DB migration tests.
- path: channeldb/migtest
linters:
- dupl
- forbidigo
- godot
# Allow fmt.Printf() in commands.
- path: cmd/commands/*
linters:
- forbidigo
# Allow fmt.Printf() in config parsing.
- path: config\.go
linters:
- forbidigo
- path: lnd\.go
linters:
- forbidigo
- path: lnmock/*
linters:
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
- path: mock*
linters:
# forcetypeassert is skipped for the mock because the test would fail
# if the returned value doesn't match the type, so there's no need to
# check the convert.
- forcetypeassert
exclusions:
generated: lax
# Skip autogenerated files for mobile and gRPC as well as copied code for
# internal use.
paths:
- third_party$
- builtin$
- examples$
- "mobile\\/.*generated\\.go"
- "\\.pb\\.go$"
- "\\.pb\\.gw\\.go$"
- "internal\\/musig2v040"
- channeldb/migration_01_to_11
- channeldb/migration/lnwire21

View file

@ -1,6 +1,6 @@
# If you change this please also update GO_VERSION in Makefile (then run
# `make lint` to see where else it needs to be updated as well).
FROM golang:1.25.3-alpine as builder
FROM golang:1.26.4-alpine as builder
# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
# queries required to connect to linked containers succeed.
@ -29,13 +29,13 @@ FROM alpine as final
VOLUME /root/.lnd
# Add utilities for quality of life and SSL-related reasons. We also require
# curl and gpg for the signature verification script.
# wget and gpg for the signature verification script.
RUN apk --no-cache add \
bash \
jq \
ca-certificates \
gnupg \
curl
wget
# Copy the binaries from the builder image.
COPY --from=builder /go/bin/lncli /bin/

View file

@ -1,16 +1,20 @@
PKG := github.com/lightningnetwork/lnd
MOBILE_PKG := $(PKG)/mobile
TOOLS_DIR := tools
TOOLS_MOD := $(TOOLS_DIR)/go.mod
GOCC ?= go
PREFIX ?= /usr/local
GOTOOL := GOWORK=off $(GOCC) tool -modfile=$(TOOLS_MOD)
BTCD_PKG := github.com/btcsuite/btcd
GOIMPORTS_PKG := github.com/rinchsan/gosimports/cmd/gosimports
GOLINT_PKG := github.com/golangci/golangci-lint/v2/cmd/golangci-lint
GO_BIN := ${GOPATH}/bin
BTCD_BIN := $(GO_BIN)/btcd
GOIMPORTS_BIN := $(GO_BIN)/gosimports
GOMOBILE_BIN := $(GO_BIN)/gomobile
MOBILE_BUILD_DIR :=${GOPATH}/src/$(MOBILE_PKG)/build
@ -18,6 +22,10 @@ IOS_BUILD_DIR := $(MOBILE_BUILD_DIR)/ios
IOS_BUILD := $(IOS_BUILD_DIR)/Lndmobile.xcframework
ANDROID_BUILD_DIR := $(MOBILE_BUILD_DIR)/android
ANDROID_BUILD := $(ANDROID_BUILD_DIR)/Lndmobile.aar
# For Android, set max page size to 16KB to support devices using 16KB memory pages.
# Reference: https://developer.android.com/guide/practices/page-sizes
ANDROID_MAX_PAGE_SIZE := 16384
ANDROID_EXTLDFLAGS := -extldflags '-Wl,-z,max-page-size=$(ANDROID_MAX_PAGE_SIZE)'
COMMIT := $(shell git describe --tags --dirty)
@ -28,7 +36,7 @@ ACTIVE_GO_VERSION_MINOR := $(shell echo $(ACTIVE_GO_VERSION) | cut -d. -f2)
# GO_VERSION is the Go version used for the release build, docker files, and
# GitHub Actions. This is the reference version for the project. All other Go
# versions are checked against this version.
GO_VERSION = 1.25.3
GO_VERSION = 1.26.4
GOBUILD := $(GOCC) build -v
GOINSTALL := $(GOCC) install -v
@ -64,12 +72,41 @@ ifneq ($(workers),)
LINT_WORKERS = --concurrency=$(workers)
endif
DOCKER_TOOLS = docker run \
# Docker cache mounting strategy:
# - CI (GitHub Actions): Use bind mounts to host paths that GA caches persist.
# - Local: Use Docker named volumes (much faster on macOS/Windows due to
# avoiding slow host-syncing overhead).
# Paths inside container must match GOCACHE/GOMODCACHE in tools/Dockerfile.
ifdef CI
# CI mode: bind mount to host paths that GitHub Actions caches.
DOCKER_TOOLS_BASE = docker run \
--rm \
-v $(shell bash -c "$(GOCC) env GOCACHE || (mkdir -p /tmp/go-cache; echo /tmp/go-cache)"):/tmp/build/.cache \
-v $(shell bash -c "$(GOCC) env GOMODCACHE || (mkdir -p /tmp/go-modcache; echo /tmp/go-modcache)"):/tmp/build/.modcache \
-v $(shell bash -c "mkdir -p /tmp/go-lint-cache; echo /tmp/go-lint-cache"):/root/.cache/golangci-lint \
-v $$(pwd):/build lnd-tools
-v $${HOME}/.cache/go-build:/tmp/build/.cache \
-v $${HOME}/go/pkg/mod:/tmp/build/.modcache \
-v $${HOME}/.cache/golangci-lint:/root/.cache/golangci-lint \
-v $$(pwd):/build
DOCKER_TOOLS = $(DOCKER_TOOLS_BASE) lnd-tools
DOCKER_TOOLS_LINT = $(DOCKER_TOOLS)
else
# Local mode: Docker named volumes for fast macOS/Windows performance.
# Detect if we're in a git worktree. Use git rev-parse --git-common-dir to get
# the path to the main git directory for the linter's diff processor to work
# correctly with the new-from-rev setting.
GIT_COMMON_DIR := $(shell \
common_dir="$$(git rev-parse --git-common-dir 2>/dev/null)"; \
if [ "$$common_dir" != ".git" ] && [ -n "$$common_dir" ]; then \
echo "$$common_dir"; \
fi)
GIT_VOLUME := $(if $(GIT_COMMON_DIR),-v "$(GIT_COMMON_DIR):$(GIT_COMMON_DIR):ro",)
DOCKER_TOOLS_BASE = docker run \
--rm \
-v lnd-go-build-cache:/tmp/build/.cache \
-v lnd-go-mod-cache:/tmp/build/.modcache \
-v lnd-go-lint-cache:/root/.cache/golangci-lint \
-v $$(pwd):/build
DOCKER_TOOLS = $(DOCKER_TOOLS_BASE) lnd-tools
DOCKER_TOOLS_LINT = $(DOCKER_TOOLS_BASE) $(GIT_VOLUME) lnd-tools
endif
GREEN := "\\033[0;32m"
NC := "\\033[0m"
@ -88,10 +125,6 @@ $(BTCD_BIN):
@$(call print, "Installing btcd.")
cd $(TOOLS_DIR); $(GOCC) install -trimpath $(BTCD_PKG)
$(GOIMPORTS_BIN):
@$(call print, "Installing goimports.")
cd $(TOOLS_DIR); $(GOCC) install -trimpath $(GOIMPORTS_PKG)
# ============
# INSTALLATION
# ============
@ -166,7 +199,11 @@ release: clean-mobile
./scripts/release.sh build-release "$(VERSION_TAG)" "$(BUILD_SYSTEM)" "$(RELEASE_TAGS)" "$(RELEASE_LDFLAGS)" "$(GO_VERSION)"
#? docker-release: Same as release but within a docker container to support reproducible builds on BSD/MacOS platforms
docker-release:
docker-release-cache:
$(call check_docker_release_cache,$(DOCKER_RELEASE_GOCACHE))
$(call check_docker_release_cache,$(DOCKER_RELEASE_GOMODCACHE))
docker-release: docker-release-cache
@$(call print, "Building release helper docker image.")
if [ "$(tag)" = "" ]; then echo "Must specify tag=<commit_or_tag>!"; exit 1; fi
@ -315,9 +352,9 @@ fuzz:
# =========
#? fmt: Format source code and fix imports
fmt: $(GOIMPORTS_BIN)
fmt:
@$(call print, "Fixing imports.")
gosimports -w $(GOFILES_NOVENDOR)
$(GOTOOL) $(GOIMPORTS_PKG) -w $(GOFILES_NOVENDOR)
@$(call print, "Formatting source.")
gofmt -l -w -s $(GOFILES_NOVENDOR)
@ -342,10 +379,28 @@ check-go-version: check-go-version-dockerfile check-go-version-yaml
#? lint-source: Run static code analysis
lint-source: docker-tools
@$(call print, "Linting source.")
$(DOCKER_TOOLS) custom-gcl run -v $(LINT_WORKERS)
$(DOCKER_TOOLS_LINT) custom-gcl run -v $(LINT_WORKERS)
#? lint-config-check: Verify that the lint config is up to date
# We use the official linter here not our custom one because for checking the
# config file it does not matter.
lint-config-check:
@$(call print, "Checking lint config is up to date.")
$(GOTOOL) $(GOLINT_PKG) config verify -v
#? lint: Run static code analysis
lint: check-go-version lint-source
lint: check-go-version lint-config-check lint-source
#? build-native-linter: Build the custom golangci-lint binary natively
build-native-linter:
@$(call print, "Building custom linter natively.")
cd tools && CGO_ENABLED=0 $(GOCC) tool $(GOLINT_PKG) custom
#? lint-native: Run static code analysis without Docker (faster on macOS)
lint-native: check-go-version lint-config-check build-native-linter
@$(call print, "Linting source (native).")
GOWORK=off ./tools/custom-gcl run -v $(LINT_WORKERS) \
--new-from-rev=$$(git merge-base HEAD master)
#? protolint: Lint proto files using protolint
protolint:
@ -447,7 +502,7 @@ macos: mobile-rpc
android: mobile-rpc
@$(call print, "Building Android library ($(ANDROID_BUILD)).")
mkdir -p $(ANDROID_BUILD_DIR)
$(GOMOBILE_BIN) bind -target=android -androidapi 21 -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS)" -v -o $(ANDROID_BUILD) $(MOBILE_PKG)
$(GOMOBILE_BIN) bind -target=android -androidapi 21 -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS) $(ANDROID_EXTLDFLAGS)" -v -o $(ANDROID_BUILD) $(MOBILE_PKG)
#? mobile: Build mobile RPC stubs and project templates for iOS and Android
mobile: ios android
@ -465,6 +520,11 @@ clean-mobile:
$(RM) -r mobile/build
$(RM) mobile/*_generated.go
#? clean-docker-volumes: Remove Docker cache volumes used for local development
clean-docker-volumes:
@$(call print, "Removing Docker cache volumes.")
docker volume rm lnd-go-build-cache lnd-go-mod-cache lnd-go-lint-cache 2>/dev/null || true
.PHONY: all \
btcd \
default \
@ -483,6 +543,7 @@ clean-mobile:
flake-unit \
fmt \
lint \
lint-native \
list \
rpc \
rpc-format \
@ -493,4 +554,5 @@ clean-mobile:
ios \
android \
mobile \
clean
clean \
clean-docker-volumes

View file

@ -94,7 +94,7 @@ any issues regarding security or privacy, please disclose the information
responsibly by sending an email to security at lightning dot engineering,
preferably encrypted using our designated PGP key
(`91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`) which can be found
[here](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/5fa96010af201628bcfa61e9309d9b13d23d220f/security@lightning.engineering).
[here](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/1ecb328bbcf36f76ead67f08008f8db1da07e60e/security@lightning.engineering).
## Further reading
* [Step-by-step send payment guide with docker](https://github.com/lightningnetwork/lnd/tree/master/docker)

View file

@ -8,4 +8,4 @@ The last major lnd release is to be considered the current support version. Give
To report security issues, send an email to security@lightning.engineering (this list isn't to be used for support).
The following key can be used to communicate sensitive information: `91FE 464C D751 01DA 6B6B  AB60 555C 6465 E5BC B3AF`.
The following key can be used to communicate sensitive information: [`91FE 464C D751 01DA 6B6B  AB60 555C 6465 E5BC B3AF`](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/1ecb328bbcf36f76ead67f08008f8db1da07e60e/security@lightning.engineering).

478
actor/README.md Normal file
View file

@ -0,0 +1,478 @@
# Actor Package
## Introduction to Actors
The actor model is a conceptual model for concurrent computation that treats
"actors" as the universal primitives of concurrent computation. Originating from
Carl Hewitt's work in the 1970s and popularized by languages like Erlang and
frameworks like Akka, actors provide a high-level abstraction for building
robust, concurrent, and distributed systems.
At its core, an actor is an independent unit of computation that encapsulates:
- **State**: An actor can maintain private state that it alone can modify.
- **Behavior**: An actor defines how it reacts to messages it receives.
- **Mailbox**: Each actor has a mailbox to queue incoming messages.
Actors communicate exclusively through asynchronous message passing. When an
actor receives a message, it can:
1. Send a finite number of messages to other actors.
2. Create a finite number of new actors.
3. Designate the behavior to be used for the next message it receives (which
can be the same behavior).
Concurrency is managed by the actor system, allowing many actors to execute
concurrently without explicit lock management by the developer for actor state.
This model inherently promotes loose coupling, as actors do not share state and
interact only through messages.
## Motivation for this Package
In large, long-lived systems like `lnd`, managing complexity, concurrency, and
component lifecycles becomes increasingly challenging. This `actor` package is
introduced to address several key motivations:
### Structured Message Passing
To move away from direct, synchronous method calls between major components,
especially where concurrency or complex state interactions are involved. Message
passing encourages clearer, more auditable interactions and helps manage
concurrent access to component state.
### Eliminating "God Structs"
Over time, systems can develop large "god structs" that hold references to
numerous sub-systems. This leads to tight coupling, makes dependency management
difficult, and can obscure the flow of control and data. Actors, by
encapsulating state and behavior and interacting via messages, help break down
these monolithic structures into more manageable, independent units.
### Decoupled Lifecycles
Often, the lifecycle of a sub-system is unnecessarily tied to a parent system,
or access to a sub-system requires traversing through a central "manager"
object. Actors can have independent lifecycles managed by an actor system,
allowing for more granular control over starting, stopping, and restarting
components.
An example of such interaction is when an RPC call needs to go through several
other structs to obtain a reference to a given sub-system, in order to make a
direct method call on that sub-system.
With the model described in this document, the RPC server just needs to know
about what is effectively an _abstract address_ of that sub-system. It can then
use that to obtain something similar to a mailbox to do the method call.
This allows for a more decoupled architecture, as the RPC server doesn't need to
know the exact "shape" of the method to call, just which message to send.
Refactors of the sub-system won't break the RPC server, as long as the message
(which can be constructed via a dedicated constructor) is the same.
---
This package provides a foundational actor framework tailored for Go, enabling
developers to build components that are easier to reason about, test, and
maintain in a concurrent environment.
## Core Concepts
Let's explore the fundamental building blocks provided by this package.
### Messages
Actors communicate by sending and receiving messages. Any type that an actor
needs to process must implement the `actor.Message` interface. A simple way to
do this is by embedding `actor.BaseMessage`:
```go
package mymodule
import "github.com/lightningnetwork/lnd/actor"
// MyRequest is a custom message type.
type MyRequest struct {
// Embed BaseMessage to satisfy the Message interface.
actor.BaseMessage
Data string
}
// MessageType returns a string identifier for this message type.
func (m *MyRequest) MessageType() string {
return "MyRequest"
}
// MyResponse might be a corresponding response type.
type MyResponse struct {
actor.BaseMessage
Reply string
}
func (m *MyResponse) MessageType() string {
return "MyResponse"
}
```
The `MessageType()` method provides a string representation of the message type,
which can be useful for debugging or routing.
### Actor Behavior
The logic of an actor (how it responds to messages) is defined by its
`ActorBehavior`. This is an interface that you implement:
```go
package actor
// ActorBehavior defines the logic for how an actor processes incoming messages.
type ActorBehavior[M Message, R any] interface {
Receive(actorCtx context.Context, msg M) fn.Result[R]
}
```
The `Receive` method passes in a caller context (useful for shutdown detection)
and the incoming message. It returns an `fn.Result[R]`, which can encapsulate
either a successful response of type `R` or an error.
For simple cases, you can use `actor.FunctionBehavior` to adapt a Go function
into an `ActorBehavior`:
```go
import (
"context"
"fmt"
"github.com/lightningnetwork/lnd/actor"
"github.com/lightningnetwork/lnd/fn/v2"
)
// myActorLogic defines the processing for MyRequest messages.
func myActorLogic(ctx context.Context, msg *MyRequest) fn.Result[*MyResponse] {
// In a real actor, you might interact with state or other services.
// The actor's context (ctx) can be checked for shutdown signals.
select {
case <-ctx.Done():
return fn.Err[*MyResponse](errors.New("actor shutting down"))
default:
}
response := &MyResponse{Reply: fmt.Sprintf("Processed: %s", msg.Data)}
return fn.Ok(response)
}
// Create a behavior from the function.
behavior := actor.NewFunctionBehavior(myActorLogic)
```
For more complex cases, you can implement the `Receive` method on a new struct,
and pass that around directly.
### Service Keys and Actor References: The Interaction Layer
Direct interaction with an actor's internal state or its concrete struct is
discouraged. Instead, communication and discovery are managed through two key
abstractions: `ServiceKey` and `ActorRef`. These provide a layer of indirection,
promoting loose coupling and location transparency (though the current
implementation is in-process).
#### `ServiceKey[M Message, R any]`
A `ServiceKey` is a type-safe identifier used for registering actors that
provide a particular service and for discovering them later. The generic type
parameters `M` (the type of message the actor handles) and `R` (the type of
response the actor produces for `Ask` operations) ensure that you discover
actors compatible with the interactions you intend to perform.
```go
// Define a service key for actors that handle MyRequest and produce MyResponse.
myServiceKey := actor.NewServiceKey[*MyRequest, *MyResponse]("my-custom-service")
// Later, this key would be used with a Receptionist (part of an ActorSystem)
// to find ActorRefs for actors offering this service.
```
#### `ActorRef[M Message, R any]`
An `ActorRef` is a lightweight, shareable reference to an actor. It's the
primary means by which you send messages to an actor. It is also generic over
the message type `M` and response type `R` that the target actor handles.
You typically obtain an `ActorRef` by looking it up in a `Receptionist` using a
`ServiceKey` (covered later when discussing the `ActorSystem`), or directly from
an actor instance via its `.Ref()` method (e.g., `sampleActor.Ref()` if you have
the `Actor` instance).
There are two main ways to send messages using an `ActorRef`:
1. **Tell (Fire-and-Forget)**: Used for sending messages when you don't need a
direct reply. The call returns immediately after attempting to enqueue the
message.
```go
// Assuming 'actorRef' is an ActorRef[*MyRequest, *MyResponse] obtained for an actor.
requestMsg := &MyRequest{Data: "A fire-and-forget message"}
actorRef.Tell(context.Background(), requestMsg)
// The message is now in the actor's mailbox (or will be shortly).
```
The `context.Context` passed to `Tell` can be used to cancel the send
operation if, for example, the actor's mailbox is full and the send would
block for too long.
2. **Ask (Request-Response)**: Used when you need a response from the actor.
This returns a `Future[R]`, which represents the eventual reply.
```go
// Assuming 'actorRef' is an ActorRef[*MyRequest, *MyResponse].
askMsg := &MyRequest{Data: "A request needing a response"}
futureResponse := actorRef.Ask(context.Background(), askMsg)
```
A `Future[R]` represents a result that will be available at some point. You
can block until it's ready using `Await`:
```go
// Await the result. It's good practice to use a context with a timeout.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result := futureResponse.Await(ctx)
response, err := result.Unpack()
if err != nil {
fmt.Printf("Ask failed: %v\n", err)
// return or handle error
} else {
fmt.Printf("Received reply: %s\n", response.Reply)
}
```
The `Future` interface also offers non-blocking ways to handle results, like
`OnComplete` (for callbacks) and `ThenApply` (for chaining transformations).
A more restricted `TellOnlyRef[M]` is also available if only fire-and-forget
semantics are required (obtained via an actor's `TellRef()` method).
### Actors
An `Actor` is the concrete entity that runs a behavior, manages a mailbox, and
has a lifecycle. You create an actor using `actor.NewActor` with an
`ActorConfig`:
```go
cfg := actor.ActorConfig[*MyRequest, *MyResponse]{
ID: "my-sample-actor",
Behavior: behavior,
MailboxSize: 10,
// Dead Letter Office (covered later)
DLO: nil,
}
sampleActor, err := actor.NewActor(cfg)
if err != nil {
// Handle invalid config (empty ID, nil behavior).
return err
}
```
An actor doesn't start processing messages until its `Start()` method is called.
This launches a dedicated goroutine for the actor.
```go
sampleActor.Start()
```
To stop an actor, you call its `Stop()` method. This cancels the actor's
internal context, causing its goroutine to clean up and exit.
```go
// Sometime later...
sampleActor.Stop()
```
## Visualizing Actor Relationships
The following diagram illustrates the primary components of the actor package
and their relationships. It provides a high-level overview of how actors are
managed, discovered, and interacted with.
```mermaid
classDiagram
direction TB
class ActorSystem {
+Receptionist
+DeadLetters
+Shutdown()
}
class Receptionist {
+Find(ServiceKey) ActorRef[]
+Register(ServiceKey, ActorRef)
}
class DeadLetterOffice {
+Receive(undeliverable Message)
}
class ServiceKey {
+Spawn(ActorSystem, Behavior) ActorRef
}
class Actor {
-mailbox
-behavior
+Ref() ActorRef
+Start()
+Stop()
}
class ActorRef {
<<Interface>>
+Tell(Message)
+Ask(Message) Future
}
class Message {
<<Interface>>
}
class Future {
+Await() Result
}
class Router {
+Tell(Message)
+Ask(Message) Future
}
%% Core system relationships
ActorSystem *-- Receptionist : has
ActorSystem *-- DeadLetterOffice : provides
ActorSystem o-- "manages" Actor
%% Actor and communication
Actor --> ActorRef : provides
Actor ..> Message : processes
ActorRef ..> Message : sends
ActorRef ..> Future : returns for Ask
%% Service discovery and routing
Receptionist o-- ServiceKey : uses for lookup
ServiceKey ..> Actor : creates
Router --> ActorRef : routes to
Router --> Receptionist : discovers actors via
note for ActorSystem "Central manager for actor lifecycle and service discovery"
note for Actor "Independent unit with encapsulated state and behavior"
note for ActorRef "Location-transparent handle for sending messages"
note for Message "Data exchanged between actors"
note for ServiceKey "Type-safe identifier for actor registration and discovery"
note for Router "Distributes messages among multiple actors"
note for DeadLetterOffice "Handles messages that cannot be delivered"
```
## The Actor System
While individual actors are useful, they often need to be managed and
coordinated. The `ActorSystem` serves this purpose.
```go
system := actor.NewActorSystem()
// Ensures all actors in the system are stopped.
defer system.Shutdown()
```
### Actor Lifecycle and Registration
The `ActorSystem` can manage the lifecycle of actors. You can register actors
with the system:
```go
// Using 'behavior' from earlier and 'myServiceKey' defined in the
// "Service Keys and Actor References" section.
// RegisterWithSystem creates, starts, and registers the actor.
actorRefFromSystem := actor.RegisterWithSystem(
system, "system-managed-actor", myServiceKey, behavior,
)
```
Alternatively, a `ServiceKey` itself provides a `Spawn` method for convenience:
```go
actorRefSpawned := myServiceKey.Spawn(system, "spawned-actor", behavior)
```
Actors registered with the system are automatically stopped when
`system.Shutdown()` is called. You can also stop and remove individual actors
using `system.StopAndRemoveActor(actorID)`.
A `ServiceKey` is essentially the mailbox address of an actor.
### Receptionist: Service Discovery
Actors often need to find other actors to communicate with. The `Receptionist`
facilitates this. Actors are registered with the receptionist using a
`ServiceKey`, which is type-safe.
```go
// Get the system's receptionist.
receptionist := system.Receptionist()
// Find actors registered for a specific service key.
foundRefs := actor.FindInReceptionist(receptionist, myServiceKey)
if len(foundRefs) > 0 {
targetActor := foundRefs[0]
targetActor.Tell(context.Background(), &MyRequest{Data: "Hello from a discoverer!"})
} else {
fmt.Println("No actors found for service key:", myServiceKey)
}
```
When an actor is stopped (e.g., via `ServiceKey.Unregister` or system shutdown),
it should also be unregistered from the receptionist.
### Dead Letter Office (DLO)
What happens to messages that cannot be delivered? For example, if an actor is
stopped while messages are still in its mailbox, or if a message is sent to an
actor that doesn't exist (though the current `ActorRef` design makes the latter
less likely for direct sends).
The `ActorSystem` provides a default `DeadLetterActor`. When an actor is
configured (via `ActorConfig.DLO`), undeliverable messages (e.g., those drained
from its mailbox upon shutdown) can be routed to this DLO. This allows for
logging, auditing, or potential manual intervention for "lost" messages.
```go
// Actors created via RegisterWithSystem or ServiceKey.Spawn
// are automatically configured to use the system's DLO.
// system.DeadLetters() returns an ActorRef to the system's DLO.
```
## Routers: Distributing Work
Sometimes, you might have multiple actors performing the same kind of task, and
you want to distribute messages among them. A `Router` can do this. It's not an
actor itself but acts as a dispatcher.
A `Router` uses a `RoutingStrategy` to pick one actor from a group registered
under a `ServiceKey`.
```go
// Assume 'system' and 'myServiceKey' are set up, and multiple actors
// are registered with 'myServiceKey'.
// Create a round-robin routing strategy.
roundRobinStrategy := actor.NewRoundRobinStrategy[*MyRequest, *MyResponse]()
// Create a router for 'myServiceKey' using this strategy.
// Messages sent to this router will be forwarded to one of the actors
// registered under 'myServiceKey'.
// The router also needs a DLO for messages it can't route (e.g., if no actors are available).
serviceRouter := actor.NewRouter(
system.Receptionist(),
myServiceKey,
roundRobinStrategy,
system.DeadLetters(),
)
// Now, interact with the router as if it were an ActorRef:
serviceRouter.Tell(context.Background(), &MyRequest{Data: "Message via router"})
futureReplyFromRouter := serviceRouter.Ask(context.Background(), &MyRequest{Data: "Ask via router"})
// ... await futureReplyFromRouter ...
```
If the router cannot find any available actors for the `ServiceKey` (e.g., none
are registered or running), `Tell` operations will typically send the message to
the router's configured DLO, and `Ask` operations will return a `Future`
completed with `ErrNoActorsAvailable`.

294
actor/actor.go Normal file
View file

@ -0,0 +1,294 @@
package actor
import (
"context"
"sync"
"github.com/lightningnetwork/lnd/fn/v2"
)
// MailboxFactory is a function type that creates a Mailbox implementation.
// It receives the actor's context and the desired capacity, allowing custom
// mailbox implementations (e.g., BackpressureMailbox) to be injected.
type MailboxFactory[M Message, R any] func(ctx context.Context,
capacity int) Mailbox[M, R]
// ActorConfig holds the configuration parameters for creating a new Actor.
// It is generic over M (Message type) and R (Response type) to accommodate
// the actor's specific behavior.
type ActorConfig[M Message, R any] struct {
// ID is the unique identifier for the actor.
ID string
// Behavior defines how the actor responds to messages.
Behavior ActorBehavior[M, R]
// DLO is a reference to the dead letter office for this actor system.
// If nil, undeliverable messages during shutdown or due to a full
// mailbox (if such logic were added) might be dropped.
DLO ActorRef[Message, any]
// MailboxSize defines the buffer capacity of the actor's mailbox.
MailboxSize int
// MailboxFactory is an optional factory for creating the actor's
// mailbox. If nil, a default ChannelMailbox will be used.
MailboxFactory MailboxFactory[M, R]
}
// envelope wraps a message with its associated promise. This allows the sender
// of an "ask" message to await a response. If the promise is nil, it
// signifies a "tell" operation (fire-and-forget).
type envelope[M Message, R any] struct {
message M
promise Promise[R]
}
// Actor represents a concrete actor implementation. It encapsulates a behavior,
// manages its internal state implicitly through that behavior, and processes
// messages from its mailbox sequentially in its own goroutine.
type Actor[M Message, R any] struct {
// id is the unique identifier for the actor.
id string
// behavior defines how the actor responds to messages.
behavior ActorBehavior[M, R]
// mailbox is the incoming message queue for the actor.
mailbox Mailbox[M, R]
// ctx is the context governing the actor's lifecycle.
ctx context.Context
// cancel is the function to cancel the actor's context.
cancel context.CancelFunc
// dlo is a reference to the dead letter office for this actor system.
dlo ActorRef[Message, any]
// startOnce ensures the actor's processing loop is started only once.
startOnce sync.Once
// stopOnce ensures the actor's processing loop is stopped only once.
stopOnce sync.Once
// ref is the cached ActorRef for this actor.
ref ActorRef[M, R]
}
// NewActor creates a new actor instance with the given ID and behavior.
// It initializes the actor's internal structures but does not start its
// message processing goroutine. The Start() method must be called to begin
// processing messages.
func NewActor[M Message, R any](cfg ActorConfig[M, R]) (*Actor[M, R],
error) {
if cfg.ID == "" {
return nil, ErrEmptyActorID
}
if cfg.Behavior == nil {
return nil, ErrNilBehavior
}
ctx, cancel := context.WithCancel(context.Background())
// Ensure MailboxSize has a sane default if not specified or zero. A
// capacity of 0 would make the channel unbuffered, which is generally
// not desired for actor mailboxes.
mailboxCapacity := cfg.MailboxSize
if mailboxCapacity <= 0 {
// Default to a small capacity if an invalid one is given. This
// could also come from a global constant.
mailboxCapacity = 1
}
// Create the mailbox using the factory if provided, otherwise use
// the default ChannelMailbox.
var mailbox Mailbox[M, R]
if cfg.MailboxFactory != nil {
mailbox = cfg.MailboxFactory(ctx, mailboxCapacity)
} else {
mailbox = NewChannelMailbox[M, R](ctx, mailboxCapacity)
}
actor := &Actor[M, R]{
id: cfg.ID,
behavior: cfg.Behavior,
mailbox: mailbox,
ctx: ctx,
cancel: cancel,
dlo: cfg.DLO,
}
// Create and cache the actor's own reference.
actor.ref = &actorRefImpl[M, R]{
actor: actor,
}
return actor, nil
}
// Start initiates the actor's message processing loop in a new goroutine. This
// method should be called once after the actor is created.
func (a *Actor[M, R]) Start() {
a.startOnce.Do(func() {
log.Infof("Actor %s: starting", a.id)
go a.process()
})
}
// process is the main event loop for the actor. It continuously monitors its
// mailbox for incoming messages and its context for cancellation signals.
func (a *Actor[M, R]) process() {
// Use the new iterator pattern for receiving messages.
for env := range a.mailbox.Receive(a.ctx) {
result := a.behavior.Receive(a.ctx, env.message)
// If a promise was provided (i.e., it was an "ask"
// operation), complete the promise with the result from
// the behavior.
if env.promise != nil {
env.promise.Complete(result)
}
}
// Context was cancelled or mailbox closed, drain remaining messages.
a.mailbox.Close()
for env := range a.mailbox.Drain() {
// If a DLO is configured, send the original message there
// for auditing or potential manual reprocessing.
if a.dlo != nil {
a.dlo.Tell(context.Background(), env.message)
}
// If it was an Ask, complete the promise with an error
// indicating the actor terminated.
if env.promise != nil {
env.promise.Complete(fn.Err[R](ErrActorTerminated))
}
}
}
// Stop signals the actor to terminate its processing loop and shut down.
// This is achieved by cancelling the actor's internal context. The actor's
// goroutine will exit once it detects the context cancellation.
func (a *Actor[M, R]) Stop() {
a.stopOnce.Do(func() {
log.Infof("Actor %s: stopping", a.id)
a.cancel()
})
}
// actorRefImpl provides a concrete implementation of the ActorRef interface. It
// holds a reference to the target Actor instance, enabling message sending.
type actorRefImpl[M Message, R any] struct {
actor *Actor[M, R]
}
// Tell sends a message without waiting for a response. If the context is
// cancelled before the message can be sent to the actor's mailbox, the message
// may be dropped.
//
//nolint:ll
func (ref *actorRefImpl[M, R]) Tell(ctx context.Context, msg M) {
// If the actor's own context is already done, don't try to send.
// Route to DLO if available.
if ref.actor.ctx.Err() != nil {
ref.trySendToDLO(msg)
return
}
env := envelope[M, R]{message: msg, promise: nil}
// Use mailbox Send method which internally checks both contexts.
if !ref.actor.mailbox.Send(ctx, env) {
// Failed to send - check if actor terminated.
if ref.actor.ctx.Err() != nil {
ref.trySendToDLO(msg)
}
// Otherwise the message was either dropped by backpressure
// (load shedding) or the caller's context was cancelled.
// Both are intentionally silent — no DLO routing.
}
}
// Ask sends a message and returns a Future for the response. The Future will be
// completed with the actor's reply or an error if the operation fails (e.g.,
// context cancellation before send).
//
//nolint:ll
func (ref *actorRefImpl[M, R]) Ask(ctx context.Context, msg M) Future[R] {
// Create a new promise that will be fulfilled with the actor's response.
promise := NewPromise[R]()
// If the actor's own context is already done, complete the promise with
// ErrActorTerminated and return immediately. This is the primary guard
// against trying to send to a stopped actor.
if ref.actor.ctx.Err() != nil {
promise.Complete(fn.Err[R](ErrActorTerminated))
return promise.Future()
}
// Check if the context is already done before attempting to send. This
// ensures deterministic behavior and prevents a race where the message
// could be enqueued even though the context was already cancelled.
if ctx.Err() != nil {
promise.Complete(fn.Err[R](ctx.Err()))
return promise.Future()
}
env := envelope[M, R]{message: msg, promise: promise}
// Use mailbox Send method which internally checks both contexts.
if !ref.actor.mailbox.Send(ctx, env) {
// Determine the error based on what failed.
switch {
case ref.actor.ctx.Err() != nil:
promise.Complete(fn.Err[R](ErrActorTerminated))
case ctx.Err() != nil:
promise.Complete(fn.Err[R](ctx.Err()))
default:
// Neither context is done — the mailbox's
// backpressure mechanism dropped the message.
promise.Complete(fn.Err[R](ErrMessageDropped))
}
}
// Return the future associated with the promise, allowing the caller to
// await the response.
return promise.Future()
}
// trySendToDLO attempts to send the message to the actor's DLO if configured.
func (ref *actorRefImpl[M, R]) trySendToDLO(msg M) {
if ref.actor.dlo != nil {
// Use context.Background() for sending to DLO as the
// original context might be done or the operation
// should not be bound by it.
// This Tell to DLO is fire-and-forget.
ref.actor.dlo.Tell(context.Background(), msg)
}
}
// ID returns the unique identifier for this actor.
func (ref *actorRefImpl[M, R]) ID() string {
return ref.actor.id
}
// Ref returns an ActorRef for this actor. This allows clients to interact with
// the actor (send messages) without having direct access to the Actor struct
// itself, promoting encapsulation and location transparency.
func (a *Actor[M, R]) Ref() ActorRef[M, R] {
return a.ref
}
// TellRef returns a TellOnlyRef for this actor. This allows clients to send
// messages to the actor using only the "tell" pattern (fire-and-forget),
// without having access to "ask" capabilities.
func (a *Actor[M, R]) TellRef() TellOnlyRef[M] {
return a.ref
}

446
actor/actor_test.go Normal file
View file

@ -0,0 +1,446 @@
package actor
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/require"
)
// testMsg is a simple message type for testing. It embeds BaseMessage to
// satisfy the actor.Message interface.
type testMsg struct {
BaseMessage
data string
replyChan chan string
}
// MessageType returns the type name of the message.
func (m *testMsg) MessageType() string {
return "testMsg"
}
// newTestMsg creates a new test message.
func newTestMsg(data string) *testMsg {
return &testMsg{data: data}
}
// newTestMsgWithReply creates a new test message that includes a reply channel.
// This can be used by test behaviors to send data back to the test
// synchronously, especially for Tell operations.
func newTestMsgWithReply(data string, replyChan chan string) *testMsg {
return &testMsg{data: data, replyChan: replyChan}
}
// echoBehavior is a simple actor behavior that processes *testMsg messages. It
// stores the last message's data and, for Ask, echoes it back. For Tell, if
// replyChan is set in testMsg, it sends data back on it.
type echoBehavior struct {
lastMsgData atomic.Value
processingDelay time.Duration
t *testing.T
}
// newEchoBehavior creates a new echoBehavior.
func newEchoBehavior(t *testing.T, delay time.Duration) *echoBehavior {
return &echoBehavior{t: t, processingDelay: delay}
}
// Receive handles incoming messages. It simulates work if processingDelay is
// set, stores the message data, and responds for Ask operations or via
// replyChan for Tell.
func (b *echoBehavior) Receive(_ context.Context,
msg *testMsg) fn.Result[string] {
if b.processingDelay > 0 {
time.Sleep(b.processingDelay)
}
b.lastMsgData.Store(msg.data)
if msg.replyChan != nil {
// Attempt to send the data on the reply channel, but quit if
// it takes longer than 1 second (e.g., channel unbuffered
// and no receiver).
select {
case msg.replyChan <- msg.data:
case <-time.After(time.Second):
b.t.Logf("warning: replyChan send timed out")
}
}
return fn.Ok(fmt.Sprintf("echo: %s", msg.data))
}
// GetLastMsgData retrieves the data from the last message processed.
func (b *echoBehavior) GetLastMsgData() (string, bool) {
val := b.lastMsgData.Load()
if val == nil {
return "", false
}
data, ok := val.(string)
return data, ok
}
// errorBehavior is an actor behavior that always returns a predefined error
// upon receiving a message.
type errorBehavior struct {
err error
}
// newErrorBehavior creates a new errorBehavior.
func newErrorBehavior(err error) *errorBehavior {
return &errorBehavior{err: err}
}
// Receive always returns the configured error.
func (b *errorBehavior) Receive(_ context.Context,
_ *testMsg) fn.Result[string] {
return fn.Err[string](b.err)
}
// blockingBehavior is an actor behavior that blocks until its actorCtx is done.
type blockingBehavior struct{}
// Receive blocks until the actor's context is cancelled, then returns the
// context's error.
func (b *blockingBehavior) Receive(actorCtx context.Context,
_ *testMsg) fn.Result[string] {
<-actorCtx.Done()
return fn.Err[string](actorCtx.Err())
}
// deadLetterTestMsg is a distinct message type used for testing DLO
// interactions.
type deadLetterTestMsg struct {
BaseMessage
id string
}
// MessageType returns the type name of the message.
func (m *deadLetterTestMsg) MessageType() string {
return "deadLetterTestMsg"
}
// deadLetterObserverBehavior is a behavior for a test Dead Letter Office actor.
// It records all messages sent to it, allowing tests to verify DLO
// interactions.
type deadLetterObserverBehavior struct {
mu sync.Mutex
receivedMsgs []Message
}
// newDeadLetterObserverBehavior creates a new deadLetterObserverBehavior.
func newDeadLetterObserverBehavior() *deadLetterObserverBehavior {
return &deadLetterObserverBehavior{
receivedMsgs: make([]Message, 0),
}
}
// Receive records the incoming message and returns a successful result.
func (b *deadLetterObserverBehavior) Receive(_ context.Context,
msg Message) fn.Result[any] {
b.mu.Lock()
b.receivedMsgs = append(b.receivedMsgs, msg)
b.mu.Unlock()
return fn.Ok[any](nil)
}
// GetReceivedMsgs returns a copy of all messages received by this DLO.
func (b *deadLetterObserverBehavior) GetReceivedMsgs() []Message {
b.mu.Lock()
defer b.mu.Unlock()
msgs := make([]Message, len(b.receivedMsgs))
copy(msgs, b.receivedMsgs)
return msgs
}
// actorTestHarness provides helper methods for setting up actors in tests. It
// manages a dedicated DLO for actors created through it.
type actorTestHarness struct {
t *testing.T
dlo *Actor[Message, any]
dloBeh *deadLetterObserverBehavior
}
// newActorTestHarness sets up a test harness with a dedicated DLO. The DLO is
// automatically stopped when the test cleans up.
func newActorTestHarness(t *testing.T) *actorTestHarness {
t.Helper()
dloBeh := newDeadLetterObserverBehavior()
dloCfg := ActorConfig[Message, any]{
ID: "test-dlo-" + t.Name(),
Behavior: dloBeh,
DLO: nil,
MailboxSize: 10,
}
dloActor, err := NewActor[Message, any](dloCfg)
require.NoError(t, err)
dloActor.Start()
t.Cleanup(dloActor.Stop)
return &actorTestHarness{
t: t,
dlo: dloActor,
dloBeh: dloBeh,
}
}
// newActor creates, starts, and registers a new actor for cleanup. The actor
// will use the harness's DLO.
func (h *actorTestHarness) newActor(id string,
beh ActorBehavior[*testMsg, string],
mailboxSize int) *Actor[*testMsg, string] {
h.t.Helper()
cfg := ActorConfig[*testMsg, string]{
ID: id,
Behavior: beh,
DLO: h.dlo.Ref(),
MailboxSize: mailboxSize,
}
actor, err := NewActor(cfg)
require.NoError(h.t, err)
actor.Start()
h.t.Cleanup(actor.Stop)
return actor
}
// assertDLOMessage checks that the DLO eventually receives a specific message.
func (h *actorTestHarness) assertDLOMessage(expectedMsg Message) {
h.t.Helper()
require.Eventually(h.t, func() bool {
msgs := h.dloBeh.GetReceivedMsgs()
for _, m := range msgs {
if reflect.DeepEqual(m, expectedMsg) {
return true
}
}
return false
}, time.Second, 10*time.Millisecond,
"dLO did not receive expected message: %v", expectedMsg,
)
}
// assertNoDLOMessages checks that the DLO has not received any messages.
func (h *actorTestHarness) assertNoDLOMessages() {
h.t.Helper()
// Allow a very brief moment for any async DLO sends to occur.
time.Sleep(20 * time.Millisecond)
msgs := h.dloBeh.GetReceivedMsgs()
require.Empty(h.t, msgs, "dLO received unexpected messages")
}
// TestActorNewActorIDAndRefs verifies that NewActor correctly initializes an
// actor's ID and provides functional ActorRef and TellOnlyRef instances.
func TestActorNewActorIDAndRefs(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
actorID := "test-actor-1"
beh := newEchoBehavior(t, 0)
actor := h.newActor(actorID, beh, 1)
require.Equal(t, actorID, actor.Ref().ID(), "actorRef ID mismatch")
require.Equal(
t, actorID, actor.TellRef().ID(), "tellOnlyRef ID mismatch",
)
require.NotNil(t, actor.Ref(), "actorRef should not be nil")
require.NotNil(t, actor.TellRef(), "tellOnlyRef should not be nil")
}
// TestActorStartStop verifies the basic lifecycle of an actor: starting,
// processing messages, and stopping.
func TestActorStartStop(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
beh := newEchoBehavior(t, 0)
actor := h.newActor("test-actor-lifecycle", beh, 1)
// Actor should be running and process a message.
msgData := "hello"
replyChan := make(chan string, 1)
actor.Ref().Tell(
context.Background(), newTestMsgWithReply(msgData, replyChan),
)
received, err := fn.RecvOrTimeout(replyChan, 100*time.Millisecond)
require.NoError(t, err, "timed out waiting for actor to process message")
require.Equal(
t, msgData, received, "actor did not process message before stop",
)
actor.Stop()
time.Sleep(50 * time.Millisecond)
// Try sending another message; it should ideally not be processed or go
// to DLO.
msgDataAfterStop := "message-after-stop"
replyChanAfterStop := make(chan string, 1)
actor.Ref().Tell(
context.Background(),
newTestMsgWithReply(msgDataAfterStop, replyChanAfterStop),
)
// We expect a timeout here, meaning the message was not processed by
// the echoBehavior's replyChan.
_, err = fn.RecvOrTimeout(replyChanAfterStop, 100*time.Millisecond)
// err == nil would mean a message was received, meaning the actor
// processed it after Stop().
require.Error(t, err, "actor processed message after Stop()")
require.ErrorContains(t, err, "timeout hit")
h.assertDLOMessage(
&testMsg{data: msgDataAfterStop, replyChan: replyChanAfterStop},
)
}
// TestActorTellBasic verifies that a message sent via Tell is processed by the
// actor's behavior.
func TestActorTellBasic(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
beh := newEchoBehavior(t, 0)
actor := h.newActor("test-actor-tell", beh, 1)
msgData := "tell-message"
replyChan := make(chan string, 1)
actor.Ref().Tell(
context.Background(), newTestMsgWithReply(msgData, replyChan),
)
receivedTell, errTell := fn.RecvOrTimeout(replyChan, 100*time.Millisecond)
require.NoError(t, errTell, "timed out waiting for Tell message processing")
require.Equal(
t, msgData, receivedTell, "behavior did not receive Tell message data",
)
lastData, ok := beh.GetLastMsgData()
require.True(t, ok, "last message data not set in behavior")
require.Equal(t, msgData, lastData, "last message data mismatch")
h.assertNoDLOMessages()
}
// TestActorAskSuccess verifies that a message sent via Ask is processed, and
// the returned Future is completed with the behavior's successful result.
func TestActorAskSuccess(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
beh := newEchoBehavior(t, 0)
actor := h.newActor("test-actor-ask-success", beh, 1)
msgData := "ask-message"
future := actor.Ref().Ask(context.Background(), newTestMsg(msgData))
result := future.Await(context.Background())
require.False(t, result.IsErr(), "ask returned an error: %v", result.Err())
result.WhenOk(func(val string) {
expectedReply := fmt.Sprintf("echo: %s", msgData)
require.Equal(t, expectedReply, val, "ask response mismatch")
})
lastData, ok := beh.GetLastMsgData()
require.True(t, ok, "last message data not set in behavior")
require.Equal(t, msgData, lastData, "last message data mismatch")
h.assertNoDLOMessages()
}
// TestActorAskErrorBehavior verifies that if an actor's behavior returns an
// error, the Future from an Ask call is completed with that error.
func TestActorAskErrorBehavior(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
expectedErr := errors.New("behavior error")
beh := newErrorBehavior(expectedErr)
actor := h.newActor("test-actor-ask-error", beh, 1)
future := actor.Ref().Ask(
context.Background(), newTestMsg("ask-error-test"),
)
result := future.Await(context.Background())
require.True(t, result.IsErr(), "ask should have returned an error")
require.ErrorIs(t, result.Err(), expectedErr, "ask error mismatch")
h.assertNoDLOMessages()
}
// TestFunctionBehaviorFromSimple verifies that FunctionBehaviorFromSimple
// correctly adapts a simple (msg) -> (result, error) function into an
// ActorBehavior, handling both success and error cases.
func TestFunctionBehaviorFromSimple(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
beh := FunctionBehaviorFromSimple(
func(msg *testMsg) (string, error) {
return "simple: " + msg.data, nil
},
)
actor := h.newActor("test-simple-success", beh, 1)
future := actor.Ref().Ask(
context.Background(), newTestMsg("hello"),
)
result := future.Await(context.Background())
require.False(
t, result.IsErr(),
"expected success, got: %v", result.Err(),
)
result.WhenOk(func(val string) {
require.Equal(t, "simple: hello", val)
})
})
t.Run("error", func(t *testing.T) {
t.Parallel()
h := newActorTestHarness(t)
expectedErr := errors.New("simple behavior error")
beh := FunctionBehaviorFromSimple(
func(msg *testMsg) (string, error) {
return "", expectedErr
},
)
actor := h.newActor("test-simple-error", beh, 1)
future := actor.Ref().Ask(
context.Background(), newTestMsg("hello"),
)
result := future.Await(context.Background())
require.True(t, result.IsErr())
require.ErrorIs(t, result.Err(), expectedErr)
})
}

View file

@ -0,0 +1,166 @@
package actor
import (
"context"
"iter"
"sync"
"sync/atomic"
"github.com/lightningnetwork/lnd/queue"
)
// BackpressureMailbox implements the Mailbox interface using a
// queue.BackpressureQueue as its core buffer. The BackpressureQueue's drop
// predicate is consulted on every Send/TrySend, allowing RED-style load
// shedding before the mailbox is full.
type BackpressureMailbox[M Message, R any] struct {
// queue is the underlying backpressure-aware buffer.
queue *queue.BackpressureQueue[envelope[M, R]]
// closed tracks whether the mailbox has been closed.
closed atomic.Bool
// mu protects Send/TrySend operations to prevent send-on-closed-channel
// panics. Close() acquires write lock, Send/TrySend acquire read lock.
mu sync.RWMutex
// closeOnce ensures Close() executes exactly once.
closeOnce sync.Once
// actorCtx is the actor's context for lifecycle management.
actorCtx context.Context
}
// NewBackpressureMailbox creates a new mailbox backed by a BackpressureQueue.
// The shouldDrop function is called with the current queue depth on every send
// attempt; if it returns true the message is silently dropped.
func NewBackpressureMailbox[M Message, R any](
actorCtx context.Context,
capacity int,
shouldDrop queue.DropCheckFunc,
) *BackpressureMailbox[M, R] {
if capacity <= 0 {
capacity = 1
}
pred := queue.AsDropPredicate[envelope[M, R]](shouldDrop)
return &BackpressureMailbox[M, R]{
queue: queue.NewBackpressureQueue(capacity, pred),
actorCtx: actorCtx,
}
}
// Send attempts to send an envelope to the mailbox. The BackpressureQueue's
// drop predicate is consulted first; if it decides to drop, false is returned
// immediately. Otherwise the send blocks until the envelope is accepted, the
// caller's context is cancelled, or the actor's context is cancelled.
func (m *BackpressureMailbox[M, R]) Send(ctx context.Context,
env envelope[M, R]) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if m.IsClosed() {
return false
}
// Create a context that is cancelled when either the caller's context
// or the actor's context is done, so that the blocking Enqueue
// respects both.
merged, cancel := context.WithCancel(ctx)
stop := context.AfterFunc(m.actorCtx, cancel)
defer stop()
defer cancel()
err := m.queue.Enqueue(merged, env)
return err == nil
}
// TrySend attempts a non-blocking send. Returns false if the drop predicate
// rejects the message, the queue is at capacity, or the mailbox is closed.
func (m *BackpressureMailbox[M, R]) TrySend(env envelope[M, R]) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if m.IsClosed() {
return false
}
return m.queue.TryEnqueue(env)
}
// Receive returns an iterator that yields envelopes from the mailbox until
// the mailbox is closed, the provided context is cancelled, or the actor's
// context is cancelled.
func (m *BackpressureMailbox[M, R]) Receive(
ctx context.Context) iter.Seq[envelope[M, R]] {
return func(yield func(envelope[M, R]) bool) {
ch := m.queue.ReceiveChan()
for {
select {
case env, ok := <-ch:
if !ok {
return
}
if !yield(env) {
return
}
case <-ctx.Done():
return
case <-m.actorCtx.Done():
return
}
}
}
}
// Close closes the mailbox, preventing new messages from being sent. Any
// remaining messages can still be consumed via Drain.
func (m *BackpressureMailbox[M, R]) Close() {
m.closeOnce.Do(func() {
m.mu.Lock()
defer m.mu.Unlock()
m.closed.Store(true)
m.queue.Close()
})
}
// IsClosed returns true if the mailbox has been closed.
func (m *BackpressureMailbox[M, R]) IsClosed() bool {
return m.closed.Load()
}
// Drain returns an iterator that yields all remaining messages in the mailbox
// after it has been closed.
func (m *BackpressureMailbox[M, R]) Drain() iter.Seq[envelope[M, R]] {
return func(yield func(envelope[M, R]) bool) {
if !m.IsClosed() {
return
}
ch := m.queue.ReceiveChan()
for {
select {
case env, ok := <-ch:
if !ok {
return
}
if !yield(env) {
return
}
default:
return
}
}
}
}

View file

@ -0,0 +1,393 @@
package actor
import (
"context"
"sync"
"testing"
"github.com/lightningnetwork/lnd/queue"
"github.com/stretchr/testify/require"
)
// Compile-time assertion that BackpressureMailbox satisfies the Mailbox
// interface.
var _ Mailbox[TestMessage, int] = (*BackpressureMailbox[TestMessage, int])(nil)
// TestBackpressureMailboxDropsWhenThresholdReached verifies that
// BackpressureMailbox drops messages when shouldDrop returns true.
func TestBackpressureMailboxDropsWhenThresholdReached(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 10
const dropThreshold = 5
shouldDrop := queue.DropCheckFunc(func(queueLen int) bool {
return queueLen >= dropThreshold
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, shouldDrop,
)
// Fill up to the drop threshold — these should all succeed.
for i := range dropThreshold {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
ok := mbox.Send(ctx, env)
require.True(t, ok, "message %d should be accepted", i)
}
// Next message should be dropped by the predicate.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 99},
}
ok := mbox.Send(ctx, env)
require.False(t, ok, "message at threshold should be dropped")
}
// TestBackpressureMailboxTrySendDrops verifies TrySend also respects the drop
// predicate.
func TestBackpressureMailboxTrySendDrops(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 10
const dropThreshold = 3
shouldDrop := queue.DropCheckFunc(func(queueLen int) bool {
return queueLen >= dropThreshold
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, shouldDrop,
)
// Fill to threshold.
for i := range dropThreshold {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
ok := mbox.TrySend(env)
require.True(t, ok, "message %d should be accepted", i)
}
// TrySend should now be rejected.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 99},
}
ok := mbox.TrySend(env)
require.False(t, ok, "TrySend at threshold should be dropped")
}
// TestBackpressureMailboxNeverDropPassesThrough verifies that a never-drop
// predicate lets all messages through (up to channel capacity).
func TestBackpressureMailboxNeverDropPassesThrough(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 5
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
// Fill the entire capacity.
for i := range capacity {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
ok := mbox.Send(ctx, env)
require.True(t, ok, "message %d should be accepted", i)
}
}
// TestBackpressureMailboxDelegatesReceive verifies that Receive yields messages
// from the underlying BackpressureQueue.
func TestBackpressureMailboxDelegatesReceive(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const capacity = 5
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
// Send two messages.
for i := range 2 {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
mbox.Send(ctx, env)
}
// Close so Receive iterator terminates after draining.
mbox.Close()
var count int
for range mbox.Receive(ctx) {
count++
}
require.Equal(t, 2, count, "should receive 2 messages")
}
// TestBackpressureMailboxDelegatesDrain verifies that Drain yields remaining
// messages after close.
func TestBackpressureMailboxDelegatesDrain(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 5
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
// Send messages and close.
for i := range 3 {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
mbox.Send(ctx, env)
}
mbox.Close()
require.True(t, mbox.IsClosed())
var count int
for range mbox.Drain() {
count++
}
require.Equal(t, 3, count, "should drain 3 messages")
}
// TestBackpressureMailboxSendRespectsActorCtx verifies that Send returns false
// when the actor context is cancelled.
func TestBackpressureMailboxSendRespectsActorCtx(t *testing.T) {
t.Parallel()
actorCtx, actorCancel := context.WithCancel(context.Background())
const capacity = 1
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
actorCtx, capacity, neverDrop,
)
// Fill the mailbox to capacity.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 1},
}
ok := mbox.Send(context.Background(), env)
require.True(t, ok)
// Cancel the actor context. The next blocking send should fail.
actorCancel()
env2 := envelope[TestMessage, int]{
message: TestMessage{Value: 2},
}
ok = mbox.Send(context.Background(), env2)
require.False(t, ok, "send should fail when actor context is cancelled")
}
// TestBackpressureMailboxReceiveAfterClose verifies that calling Receive after
// Close does not panic and yields no messages (the channel is already drained).
func TestBackpressureMailboxReceiveAfterClose(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 5
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
mbox.Close()
// First Receive after close should return immediately (closed channel).
var count int
for range mbox.Receive(ctx) {
count++
}
require.Equal(t, 0, count, "no messages expected")
// Second Receive must not panic.
for range mbox.Receive(ctx) {
count++
}
require.Equal(t, 0, count, "still no messages expected")
}
// TestBackpressureMailboxDrainAfterDrain verifies that calling Drain twice
// after Close does not panic.
func TestBackpressureMailboxDrainAfterDrain(t *testing.T) {
t.Parallel()
ctx := context.Background()
const capacity = 5
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
// Send one message and close.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 1},
}
mbox.Send(ctx, env)
mbox.Close()
// First drain should yield the message.
var count int
for range mbox.Drain() {
count++
}
require.Equal(t, 1, count, "should drain 1 message")
// Second drain must not panic and should yield nothing.
count = 0
for range mbox.Drain() {
count++
}
require.Equal(t, 0, count, "second drain should yield nothing")
}
// TestBackpressureMailboxConcurrentSendClose tests concurrent Send/TrySend and
// Close operations to ensure no race conditions or panics occur.
func TestBackpressureMailboxConcurrentSendClose(t *testing.T) {
t.Parallel()
const (
numSenders = 50
capacity = 20
)
ctx := context.Background()
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, capacity, neverDrop,
)
var wg sync.WaitGroup
// Launch many goroutines that continuously call Send/TrySend.
for i := range numSenders {
wg.Add(1)
go func() {
defer wg.Done()
for j := range 100 {
env := envelope[TestMessage, int]{
message: TestMessage{
Value: i*100 + j,
},
}
// Send must not panic regardless of
// whether Close has been called.
mbox.Send(ctx, env)
}
}()
// Launch a goroutine that also calls TrySend
// concurrently.
wg.Add(1)
go func() {
defer wg.Done()
for j := range 500 {
env := envelope[TestMessage, int]{
message: TestMessage{Value: j},
}
mbox.TrySend(env)
}
}()
}
// Drain messages concurrently to free buffer space so Send
// goroutines make progress and don't all block.
wg.Add(1)
go func() {
defer wg.Done()
ch := mbox.queue.ReceiveChan()
for range ch {
}
}()
// Close the mailbox while senders are still active.
mbox.Close()
// Wait for all goroutines to finish. If the RWMutex protocol
// is broken, this test will panic with "send on closed channel"
// or the race detector will flag a data race.
wg.Wait()
require.True(t, mbox.IsClosed())
// After Close, all subsequent sends must return false.
env := envelope[TestMessage, int]{
message: TestMessage{Value: -1},
}
require.False(t, mbox.Send(ctx, env))
require.False(t, mbox.TrySend(env))
}
// TestBackpressureMailboxConcurrentMultiClose verifies that calling Close
// from multiple goroutines simultaneously does not panic.
func TestBackpressureMailboxConcurrentMultiClose(t *testing.T) {
t.Parallel()
ctx := context.Background()
neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
return false
})
mbox := NewBackpressureMailbox[TestMessage, int](
ctx, 10, neverDrop,
)
// Send a few messages first.
for i := range 5 {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
}
mbox.Send(ctx, env)
}
// Close from many goroutines simultaneously.
var wg sync.WaitGroup
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
mbox.Close()
}()
}
wg.Wait()
require.True(t, mbox.IsClosed())
}

View file

@ -0,0 +1,101 @@
package actor_test
import (
"context"
"fmt"
"time"
"github.com/lightningnetwork/lnd/actor"
"github.com/lightningnetwork/lnd/fn/v2"
)
// BasicGreetingMsg is a simple message type for the basic actor example.
type BasicGreetingMsg struct {
actor.BaseMessage
Name string
}
// MessageType implements actor.Message.
func (m BasicGreetingMsg) MessageType() string { return "BasicGreetingMsg" }
// BasicGreetingResponse is a simple response type.
type BasicGreetingResponse struct {
Greeting string
}
// ExampleActor demonstrates creating a single actor, sending it a message
// directly using Ask, and then unregistering and stopping it.
func ExampleActor() {
system := actor.NewActorSystem()
defer system.Shutdown()
//nolint:ll
greeterKey := actor.NewServiceKey[BasicGreetingMsg, BasicGreetingResponse](
"basic-greeter",
)
actorID := "my-greeter"
greeterBehavior := actor.NewFunctionBehavior(
func(ctx context.Context,
msg BasicGreetingMsg) fn.Result[BasicGreetingResponse] {
return fn.Ok(BasicGreetingResponse{
Greeting: "Hello, " + msg.Name + " from " +
actorID,
})
},
)
// Spawn the actor. This registers it with the system and receptionist,
// and starts it. It returns an ActorRef.
greeterRef, err := greeterKey.Spawn(system, actorID, greeterBehavior)
if err != nil {
fmt.Printf("Failed to spawn actor: %v\n", err)
return
}
fmt.Printf("Actor %s spawned.\n", greeterRef.ID())
// Send a message directly to the actor's reference.
askCtx, askCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
defer askCancel()
futureResponse := greeterRef.Ask(
askCtx, BasicGreetingMsg{Name: "World"},
)
awaitCtx, awaitCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
defer awaitCancel()
result := futureResponse.Await(awaitCtx)
result.WhenErr(func(err error) {
fmt.Printf("Error awaiting response: %v\n", err)
})
result.WhenOk(func(response BasicGreetingResponse) {
fmt.Printf("Received: %s\n", response.Greeting)
})
// Unregister the actor. This also stops the actor.
unregistered := greeterKey.Unregister(system, greeterRef)
if unregistered {
fmt.Printf("Actor %s unregistered and stopped.\n",
greeterRef.ID())
} else {
fmt.Printf("Failed to unregister actor %s.\n", greeterRef.ID())
}
// Verify it's no longer in the receptionist.
refsAfterUnregister := actor.FindInReceptionist(
system.Receptionist(), greeterKey,
)
fmt.Printf("Actors for key '%s' after unregister: %d\n",
"basic-greeter", len(refsAfterUnregister))
// Output:
// Actor my-greeter spawned.
// Received: Hello, World from my-greeter
// Actor my-greeter unregistered and stopped.
// Actors for key 'basic-greeter' after unregister: 0
}

View file

@ -0,0 +1,120 @@
package actor_test
import (
"context"
"fmt"
"time"
"github.com/lightningnetwork/lnd/actor"
"github.com/lightningnetwork/lnd/fn/v2"
)
// RouterGreetingMsg is a message type for the router example.
type RouterGreetingMsg struct {
actor.BaseMessage
Name string
}
// MessageType implements actor.Message.
func (m RouterGreetingMsg) MessageType() string { return "RouterGreetingMsg" }
// RouterGreetingResponse is a response type for the router example.
type RouterGreetingResponse struct {
Greeting string
HandlerID string
}
// ExampleRouter demonstrates creating multiple actors under the same service
// key and using a router to dispatch messages to them.
func ExampleRouter() {
system := actor.NewActorSystem()
defer system.Shutdown()
//nolint:ll
routerGreeterKey := actor.NewServiceKey[RouterGreetingMsg, RouterGreetingResponse](
"router-greeter-service",
)
// Behavior for the first greeter actor.
actorID1 := "router-greeter-1"
greeterBehavior1 := actor.NewFunctionBehavior(
func(ctx context.Context,
msg RouterGreetingMsg) fn.Result[RouterGreetingResponse] {
return fn.Ok(RouterGreetingResponse{
Greeting: "Greetings, " + msg.Name + "!",
HandlerID: actorID1,
})
},
)
_, err := routerGreeterKey.Spawn(system, actorID1, greeterBehavior1)
if err != nil {
fmt.Printf("Failed to spawn actor: %v\n", err)
return
}
fmt.Printf("Actor %s spawned.\n", actorID1)
// Behavior for the second greeter actor.
actorID2 := "router-greeter-2"
greeterBehavior2 := actor.NewFunctionBehavior(
func(ctx context.Context,
msg RouterGreetingMsg) fn.Result[RouterGreetingResponse] {
return fn.Ok(RouterGreetingResponse{
Greeting: "Salutations, " + msg.Name + "!",
HandlerID: actorID2,
})
},
)
_, err = routerGreeterKey.Spawn(system, actorID2, greeterBehavior2)
if err != nil {
fmt.Printf("Failed to spawn actor: %v\n", err)
return
}
fmt.Printf("Actor %s spawned.\n", actorID2)
// Create a router for the "router-greeter-service".
greeterRouter := actor.NewRouter(
system.Receptionist(), routerGreeterKey,
actor.NewRoundRobinStrategy[RouterGreetingMsg,
RouterGreetingResponse](),
system.DeadLetters(),
)
fmt.Printf("Router %s created for service key '%s'.\n",
greeterRouter.ID(), "router-greeter-service")
// Send messages through the router.
names := []string{"Alice", "Bob", "Charlie", "David"}
for _, name := range names {
askCtx, askCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
futureResponse := greeterRouter.Ask(
askCtx, RouterGreetingMsg{Name: name},
)
awaitCtx, awaitCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
result := futureResponse.Await(awaitCtx)
result.WhenErr(func(err error) {
fmt.Printf("For %s: Error - %v\n", name, err)
})
result.WhenOk(func(response RouterGreetingResponse) {
fmt.Printf("For %s: Received '%s' from %s\n",
name, response.Greeting, response.HandlerID)
})
awaitCancel()
askCancel()
}
// Output:
// Actor router-greeter-1 spawned.
// Actor router-greeter-2 spawned.
// Router router(router-greeter-service) created for service key 'router-greeter-service'.
// For Alice: Received 'Greetings, Alice!' from router-greeter-1
// For Bob: Received 'Salutations, Bob!' from router-greeter-2
// For Charlie: Received 'Greetings, Charlie!' from router-greeter-1
// For David: Received 'Salutations, David!' from router-greeter-2
}

View file

@ -0,0 +1,153 @@
package actor_test
import (
"context"
"fmt"
"time"
"github.com/lightningnetwork/lnd/actor"
"github.com/lightningnetwork/lnd/fn/v2"
)
// CounterMsg is a message type for the stateful counter actor.
// It can be used to increment the counter or get its current value.
type CounterMsg struct {
actor.BaseMessage
Increment int
GetValue bool
Who string
}
// MessageType implements actor.Message.
func (m CounterMsg) MessageType() string { return "CounterMsg" }
// CounterResponse is a response type for the counter actor.
type CounterResponse struct {
Value int
Responder string
}
// StatefulCounterActor demonstrates an actor that maintains internal state (a
// counter) and processes messages to modify or query that state.
type StatefulCounterActor struct {
counter int
actorID string
}
// NewStatefulCounterActor creates a new counter actor.
func NewStatefulCounterActor(id string) *StatefulCounterActor {
return &StatefulCounterActor{
actorID: id,
}
}
// Receive is the message handler for the StatefulCounterActor.
// It implements the actor.ActorBehavior interface implicitly when wrapped.
func (s *StatefulCounterActor) Receive(ctx context.Context,
msg CounterMsg) fn.Result[CounterResponse] {
if msg.Increment > 0 {
// For increment, we can just acknowledge or return the new
// value. Messages are sent serially, so we don't need to worry
// about a mutex here.
s.counter += msg.Increment
return fn.Ok(CounterResponse{
Value: s.counter,
Responder: s.actorID,
})
}
if msg.GetValue {
return fn.Ok(CounterResponse{
Value: s.counter,
Responder: s.actorID,
})
}
return fn.Err[CounterResponse](fmt.Errorf("invalid CounterMsg"))
}
// ExampleActor_stateful demonstrates creating an actor whose behavior is defined
// by a struct with methods, allowing it to maintain internal state.
func ExampleActor_stateful() {
system := actor.NewActorSystem()
defer system.Shutdown()
counterServiceKey := actor.NewServiceKey[CounterMsg, CounterResponse](
"struct-counter-service",
)
// Create an instance of our stateful actor logic.
actorID := "counter-actor-1"
counterLogic := NewStatefulCounterActor(actorID)
// Spawn the actor.
// The counterLogic instance itself satisfies the ActorBehavior
// interface because its Receive method matches the required signature.
counterRef, err := counterServiceKey.Spawn(
system, actorID, counterLogic,
)
if err != nil {
fmt.Printf("Failed to spawn actor: %v\n", err)
return
}
fmt.Printf("Actor %s spawned.\n", counterRef.ID())
// Send messages to increment the counter.
for i := 1; i <= 3; i++ {
askCtx, askCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
futureResp := counterRef.Ask(askCtx,
CounterMsg{
Increment: i,
Who: fmt.Sprintf("Incrementer-%d", i),
},
)
awaitCtx, awaitCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
resp := futureResp.Await(awaitCtx)
resp.WhenOk(func(r CounterResponse) {
fmt.Printf("Incremented by %d, new value: %d "+
"(from %s)\n", i, r.Value, r.Responder)
})
resp.WhenErr(func(e error) {
fmt.Printf("Error incrementing: %v\n", e)
})
awaitCancel()
askCancel()
}
// Send a message to get the current value.
askCtx, askCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
futureResp := counterRef.Ask(
askCtx, CounterMsg{GetValue: true, Who: "Getter"},
)
awaitCtx, awaitCancel := context.WithTimeout(
context.Background(), 1*time.Second,
)
finalValueResp := futureResp.Await(awaitCtx)
finalValueResp.WhenOk(func(r CounterResponse) {
fmt.Printf("Final counter value: %d (from %s)\n",
r.Value, r.Responder)
})
finalValueResp.WhenErr(func(e error) {
fmt.Printf("Error getting value: %v\n", e)
})
awaitCancel()
askCancel()
// Output:
// Actor counter-actor-1 spawned.
// Incremented by 1, new value: 1 (from counter-actor-1)
// Incremented by 2, new value: 3 (from counter-actor-1)
// Incremented by 3, new value: 6 (from counter-actor-1)
// Final counter value: 6 (from counter-actor-1)
}

View file

@ -0,0 +1,137 @@
package actor_test
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/lightningnetwork/lnd/actor"
"github.com/lightningnetwork/lnd/fn/v2"
)
// LogMsg is a message type for the TellOnly example.
type LogMsg struct {
actor.BaseMessage
Text string
}
// MessageType implements actor.Message.
func (m LogMsg) MessageType() string { return "LogMsg" }
// LoggerActorBehavior is a simple actor behavior that logs messages. It doesn't
// produce a meaningful response for Ask, so it's a good candidate for TellOnly
// interactions.
type LoggerActorBehavior struct {
mu sync.Mutex
logs []string
actorID string
}
func NewLoggerActorBehavior(id string) *LoggerActorBehavior {
return &LoggerActorBehavior{actorID: id}
}
// Receive processes LogMsg messages by appending them to an internal log. The
// response type is 'any' as it's not typically used with Ask.
func (l *LoggerActorBehavior) Receive(ctx context.Context,
msg actor.Message) fn.Result[any] {
logMessage, ok := msg.(LogMsg)
if !ok {
return fn.Err[any](fmt.Errorf("unexpected message "+
"type: %s", msg.MessageType()))
}
l.mu.Lock()
defer l.mu.Unlock()
entry := fmt.Sprintf("[%s from %s]: %s", time.Now().Format("15:04:05"),
l.actorID, logMessage.Text)
l.logs = append(l.logs, entry)
// For Tell, the result is often ignored, but we must return something.
return fn.Ok[any](nil)
}
func (l *LoggerActorBehavior) GetLogs() []string {
l.mu.Lock()
defer l.mu.Unlock()
copiedLogs := make([]string, len(l.logs))
copy(copiedLogs, l.logs)
return copiedLogs
}
// ExampleTellOnlyRef demonstrates using a TellOnlyRef for fire-and-forget
// messaging with an actor.
func ExampleTellOnlyRef() {
system := actor.NewActorSystem()
defer system.Shutdown()
// The logger actor doesn't really have a response type for Ask, so we
// use 'any'.
loggerServiceKey := actor.NewServiceKey[actor.Message, any](
"tell-only-logger-service",
)
actorID := "my-logger"
loggerLogic := NewLoggerActorBehavior(actorID)
// Spawn the actor.
fullRef, err := loggerServiceKey.Spawn(system, actorID, loggerLogic)
if err != nil {
fmt.Printf("Failed to spawn actor: %v\n", err)
return
}
fmt.Printf("Actor %s spawned.\n", fullRef.ID())
// Get a TellOnlyRef for the actor. We can get this from the Actor
// instance itself if we had it, or by type assertion if we know the
// underlying ref supports it. Since fullRef is ActorRef[actor.Message,
// any], it already satisfies TellOnlyRef[actor.Message].
//
// Or, if we had the *Actor instance: tellOnlyLogger =
// actorInstance.TellRef()
var tellOnlyLogger actor.TellOnlyRef[actor.Message] = fullRef
fmt.Printf("Obtained TellOnlyRef for %s.\n", tellOnlyLogger.ID())
// Send messages using Tell.
tellOnlyLogger.Tell(
context.Background(), LogMsg{Text: "First log entry."},
)
tellOnlyLogger.Tell(
context.Background(), LogMsg{Text: "Second log entry."},
)
// Allow some time for messages to be processed.
time.Sleep(10 * time.Millisecond)
// Retrieve logs directly from the behavior for verification in this
// example. In a real scenario, this might not be possible or desired.
logs := loggerLogic.GetLogs()
fmt.Println("Logged entries:")
for _, entry := range logs {
// Strip the timestamp and actor ID for consistent example
// output. Example entry: "[15:04:05 from my-logger]: Actual log
// text"
parts := strings.SplitN(entry, "]: ", 2)
if len(parts) == 2 {
fmt.Println(parts[1])
}
}
// Attempting to Ask using tellOnlyLogger would be a compile-time error:
// tellOnlyLogger.Ask(context.Background(), LogMsg{Text: "This would
// fail"})
// Output:
// Actor my-logger spawned.
// Obtained TellOnlyRef for my-logger.
// Logged entries:
// First log entry.
// Second log entry.
}

45
actor/func_actor.go Normal file
View file

@ -0,0 +1,45 @@
package actor
import (
"context"
"github.com/lightningnetwork/lnd/fn/v2"
)
// ActorFunc is a function type that represents an actor which functions purely
// based on a simple function processor.
type ActorFunc[M Message, R any] func(context.Context, M) fn.Result[R]
// FunctionBehavior adapts a function to the ActorBehavior interface.
type FunctionBehavior[M Message, R any] struct {
fn ActorFunc[M, R]
}
// NewFunctionBehavior creates a behavior from a function.
func NewFunctionBehavior[M Message, R any](
fn ActorFunc[M, R]) *FunctionBehavior[M, R] {
return &FunctionBehavior[M, R]{fn: fn}
}
// Receive implements ActorBehavior interface for the function.
//
// TODO(roasbeef): just base it off the function direct instead?
func (b *FunctionBehavior[M, R]) Receive(ctx context.Context,
msg M) fn.Result[R] {
return b.fn(ctx, msg)
}
// FunctionBehaviorFromSimple adapts a simpler function to the ActorBehavior
// interface.
func FunctionBehaviorFromSimple[M Message, R any](
sFunc func(M) (R, error)) *FunctionBehavior[M, R] {
return NewFunctionBehavior(
func(ctx context.Context, msg M) fn.Result[R] {
val, err := sFunc(msg)
return fn.NewResult(val, err)
},
)
}

174
actor/future.go Normal file
View file

@ -0,0 +1,174 @@
package actor
import (
"context"
"sync"
"sync/atomic"
"github.com/lightningnetwork/lnd/fn/v2"
)
// promiseImpl is a structure that can be used to complete a Future. It provides
// methods to set the result of an asynchronous operation and to obtain the
// Future interface for consumers.
// The promiseImpl itself is not typically exposed directly to consumers of the
// future's result; they interact with the Future interface.
type promiseImpl[T any] struct {
fut *futureImpl[T]
}
// CompleteWith completes a promise with the given value, wrapping it as a
// successful result. This is a convenience wrapper over
// promise.Complete(fn.Ok(val)). Safe to call multiple times; only the first
// call takes effect.
func CompleteWith[T any](p Promise[T], val T) {
p.Complete(fn.Ok(val))
}
// AwaitFuture blocks until the future resolves or the context is cancelled.
// On success, it returns the resolved value and a nil error. If the context
// is cancelled before the future resolves, it returns the zero value of T and
// the context cancellation error.
func AwaitFuture[T any](ctx context.Context, f Future[T]) (T, error) {
return f.Await(ctx).Unpack()
}
// NewPromise creates a new Promise. The associated Future, which consumers can
// use to await the result, can be obtained via the Future() method. The Future
// is completed by calling the Complete() method on this Promise.
func NewPromise[T any]() Promise[T] {
return &promiseImpl[T]{
fut: &futureImpl[T]{
// done is a channel that will be closed when the future
// is completed.
done: make(chan struct{}),
},
}
}
// Future returns the Future interface associated with this Promise. Consumers
// can use this to Await the result or register callbacks.
func (p *promiseImpl[T]) Future() Future[T] {
return p.fut
}
// Complete attempts to set the result of the future. It returns true if this
// call successfully set the result (i.e., it was the first to complete it),
// and false if the future had already been completed. This ensures that a
// future can only be completed once. The completion involves storing the result
// and signaling any goroutines waiting on the future's done channel.
func (p *promiseImpl[T]) Complete(result fn.Result[T]) bool {
var success bool
p.fut.completeOnce.Do(func() {
p.fut.resultCache.Store(&result)
close(p.fut.done)
success = true
})
return success
}
// futureImpl is the concrete implementation of the Future interface. It manages
// the state of an asynchronous computation's result.
type futureImpl[T any] struct {
// resultCache stores the fn.Result[T] after the future is completed.
// It's of type atomic.Pointer to allow lock-free reads after completion
// with improved type safety over atomic.Value.
resultCache atomic.Pointer[fn.Result[T]]
// done is closed once the future is completed, signaling any waiting
// Await calls.
done chan struct{}
// completeOnce ensures that the logic to set the result and close the
// done channel is executed only once.
completeOnce sync.Once
}
// Await blocks until the result is available or the passed context is
// cancelled. If the future is already completed, it returns the result
// immediately. Otherwise, it waits for either the future's completion or the
// context's cancellation.
func (f *futureImpl[T]) Await(ctx context.Context) fn.Result[T] {
// First, try a non-blocking load from the cache. If the future is
// already completed, this will return the result directly.
if resPtr := f.resultCache.Load(); resPtr != nil {
return *resPtr
}
// Wait for either the future to be done or the context to be cancelled.
select {
case <-f.done:
// The future has been completed. Load the result from the
// cache. It must be present now. Load and dereference.
// This load is safe because the 'done' channel is closed only
// after the resultCache is written (ensured by completeOnce).
resPtr := f.resultCache.Load()
// resPtr should not be nil here as <-f.done was signaled.
return *resPtr
case <-ctx.Done():
// The waiting context was cancelled before the future completed.
return fn.Err[T](ctx.Err())
}
}
// ThenApply registers a function to transform the result of a future. The
// original future is not modified; a new Future instance representing the
// transformed result is returned. Once the original future completes
// successfully, the provided transformation function (fApply) is called with
// the result. The transformation is applied asynchronously in a new goroutine.
// If the passed context is cancelled while waiting for the
// original future to complete, the returned future will yield the context's
// error.
func (f *futureImpl[T]) ThenApply(ctx context.Context,
fApply func(T) T) Future[T] {
// Create a new promise for the transformed result.
transformedPromise := NewPromise[T]()
go func() {
// Await the original future's result, respecting the passed
// context for cancellation.
originalResult := f.Await(ctx)
// If the original future completed with an error (or Await was
// cancelled by its context), complete the transformed future
// with the same error.
// This also handles the case where originalResult.Await(ctx)
// itself returned ctx.Err().
if originalResult.IsErr() {
transformedPromise.Complete(originalResult)
return
}
// Otherwise, the original future completed successfully. Apply the
// transformation function to its result.
originalResult.WhenOk(func(res T) {
newValue := fApply(res)
transformedPromise.Complete(fn.Ok(newValue))
})
}()
return transformedPromise.Future()
}
// OnComplete registers a function to be called when the result is ready. If the
// passed context is cancelled before the future completes, the callback
// function (cFunc) will be invoked with the context's error. The callback is
// executed in a new goroutine, so it does not block the completion path of the
// original future.
func (f *futureImpl[T]) OnComplete(ctx context.Context,
cFunc func(fn.Result[T])) {
go func() {
// Await the original future's result, respecting the passed
// context for cancellation.
result := f.Await(ctx)
// Call the callback function with the result.
cFunc(result)
}()
}

525
actor/future_test.go Normal file
View file

@ -0,0 +1,525 @@
package actor
import (
"context"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
// TestFutureAwaitContextCancellation tests that Await respects context
// cancellation if the context is cancelled before the future resolves.
func TestFutureAwaitContextCancellation(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
// Test cancellation when the Await context is cancelled via
// context.Cancel. The underlying future will not be completed, allowing
// us to test the cancellation path of Await.
prom1 := NewPromise[int]()
fut1 := prom1.Future()
ctx1, cancel1 := context.WithCancel(context.Background())
// We'll cancel the future immediately after creating it.
cancel1()
result1 := fut1.Await(ctx1)
require.True(t, result1.IsErr())
require.ErrorIs(
t, result1.Err(), context.Canceled,
"await with immediate cancel",
)
// Test cancellation when the Await context times out. The
// underlying future will also not be completed.
prom2 := NewPromise[int]()
fut2 := prom2.Future()
// Use a very short timeout that will trigger.
ctx2, cancel2 := context.WithTimeout(
context.Background(), 1*time.Nanosecond,
)
defer cancel2()
// Await the future; it should fall through to the timeout
// because the future itself is not completed.
result2 := fut2.Await(ctx2)
require.True(t, result2.IsErr())
require.ErrorIs(
t, result2.Err(), context.DeadlineExceeded,
"await with timeout",
)
})
}
// TestFutureAwaitFutureCompletes tests that Await returns the future's
// result if the context is not cancelled before the future resolves.
func TestFutureAwaitFutureCompletes(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
valToSet := rapid.Int().Draw(t, "valToSet")
// With a 50% chance, configure the test to complete the future
// with an error instead of a successful value.
var errToSet error
if rapid.Bool().Draw(t, "have_error") {
errToSet = fmt.Errorf("err")
}
promise := NewPromise[int]()
fut := promise.Future()
// Use a background context for Await, as we expect the future
// to complete normally.
ctx := context.Background()
// Complete the future in a separate goroutine to simulate an
// asynchronous operation.
go func() {
if errToSet != nil {
promise.Complete(fn.Err[int](errToSet))
} else {
promise.Complete(fn.Ok(valToSet))
}
}()
// Now we'll wait for the future to complete, then verify below
// that the result (value or error) is as expected.
result := fut.Await(ctx)
if errToSet != nil {
// If an error was set, verify that Await returns that
// specific error.
require.True(t, result.IsErr())
require.ErrorIs(
t, result.Err(), errToSet,
"await with error",
)
} else {
// If no error was set, verify that Await returns the
// correct value.
require.False(t, result.IsErr(), "await with value")
result.WhenOk(func(val int) {
require.Equal(
t, valToSet, val, "await with value",
)
})
}
})
}
// TestFutureThenApplyContextCancellation tests that ThenApply respects its
// context, yielding a context error if cancelled before the original future
// completes.
func TestFutureThenApplyContextCancellation(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
// The original future will not be completed in this test case,
// allowing us to specifically test the cancellation behavior of
// the context passed to ThenApply.
originalPromise := NewPromise[int]()
originalFut := originalPromise.Future()
// Create a context for ThenApply and cancel it immediately.
ctxApply, cancelApply := context.WithCancel(
context.Background(),
)
cancelApply()
var transformCalled atomic.Bool
transform := func(i int) int {
transformCalled.Store(true)
return i * 2
}
// Register the transformation. The ThenApply operation itself
// will start a goroutine to await the originalFut.
newFut := originalFut.ThenApply(ctxApply, transform)
// Await the new (transformed) future. Use a background context
// for this Await to isolate the test to the cancellation of
// ctxApply.
result := newFut.Await(context.Background())
require.True(t, result.IsErr())
require.ErrorIs(
t, result.Err(), context.Canceled,
"ThenApply with cancelled context",
)
require.False(
t, transformCalled.Load(),
"ThenApply transform function called despite "+
"context cancellation",
)
})
}
// TestFutureThenApplyOriginalFutureCompletes tests ThenApply's behavior when
// the original future completes (with a value or error) before ThenApply's
// context is cancelled.
func TestFutureThenApplyOriginalFutureCompletes(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
initialVal := rapid.Int().Draw(t, "initialVal")
// Configure whether the original future completes with an error
// or a successful value.
var originalErr error
if rapid.Bool().Draw(t, "have_error") {
originalErr = fmt.Errorf("original error")
}
originalPromise := NewPromise[int]()
originalFut := originalPromise.Future()
// Create a context for ThenApply that should not cancel before
// the original future completes.
ctxApply, cancelApply := context.WithTimeout(
context.Background(), 50*time.Millisecond,
)
defer cancelApply()
var transformCalled atomic.Bool
transform := func(i int) int {
transformCalled.Store(true)
return i * 2
}
newFut := originalFut.ThenApply(ctxApply, transform)
// Complete the original future in a separate goroutine to
// simulate asynchrony.
go func() {
if originalErr != nil {
originalPromise.Complete(
fn.Err[int](originalErr),
)
} else {
originalPromise.Complete(fn.Ok(initialVal))
}
}()
// Await our new future which transforms the original future's
// result. Use a background context for this Await.
result := newFut.Await(context.Background())
if originalErr != nil {
// If the original future had an error, the transformed
// future should also yield that same error.
require.True(t, result.IsErr())
require.ErrorIs(
t, result.Err(), originalErr,
"ThenApply with original error",
)
require.False(
t, transformCalled.Load(),
"ThenApply transform function called despite "+
"original future having an error",
)
} else {
// If the original future completed successfully, the
// transformed future should contain the transformed value.
require.False(
t, result.IsErr(),
"ThenApply with original value",
)
require.True(
t, transformCalled.Load(),
"ThenApply transform function not called for "+
"successful original future",
)
result.WhenOk(func(val int) {
expectedTransformedVal := initialVal * 2
require.Equal(
t, expectedTransformedVal, val,
"ThenApply with original value",
)
})
}
})
}
// TestFutureOnCompleteContextCancellation tests that OnComplete's callback
// receives a context error if its context is cancelled before the future
// completes.
func TestFutureOnCompleteContextCancellation(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
// The original future will not complete in this test, allowing
// us to focus on the cancellation of OnComplete's context.
originalPromise := NewPromise[int]()
originalFut := originalPromise.Future()
// Create a context for OnComplete and cancel it immediately to
// simulate a premature cancellation.
ctxComplete, cancelComplete := context.WithCancel(
context.Background(),
)
cancelComplete()
var wg sync.WaitGroup
wg.Add(1)
var (
callbackInvoked atomic.Bool
callbackResultValue fn.Result[int]
// mu is a mutex to protect callbackResultValue as it's
// written by the callback goroutine and read by the
// test goroutine.
mu sync.Mutex
)
// Register an OnComplete callback. The callback itself runs in
// a new goroutine started by OnComplete.
originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) {
mu.Lock()
callbackResultValue = res
mu.Unlock()
callbackInvoked.Store(true)
wg.Done()
})
// Use a wait group and a channel to wait for the callback to
// be invoked.
waitChan := make(chan struct{})
go func() {
wg.Wait()
close(waitChan)
}()
select {
// The callback should be invoked, even if with a context error.
case <-waitChan:
case <-time.After(50 * time.Millisecond):
require.Fail(
t, "OnComplete callback timed out waiting "+
"for execution after context cancel",
)
}
require.True(
t, callbackInvoked.Load(),
"OnComplete callback not invoked",
)
mu.Lock()
defer mu.Unlock()
// Verify that the callback received a context.Canceled error
// because its context (ctxComplete) was cancelled.
require.True(t, callbackResultValue.IsErr())
require.ErrorIs(
t, callbackResultValue.Err(), context.Canceled,
"OnComplete with cancelled context",
)
})
}
// TestFutureOnCompleteFutureCompletes tests OnComplete's behavior when the
// future completes (with value or error) before its context is cancelled.
func TestFutureOnCompleteFutureCompletes(t *testing.T) {
t.Parallel()
rapid.Check(t, func(t *rapid.T) {
valToSet := rapid.Int().Draw(t, "valToSet")
// Configure whether the original future completes with an error
// or a successful value.
var originalErr error
if rapid.Bool().Draw(t, "have_error") {
originalErr = fmt.Errorf("original error")
}
originalPromise := NewPromise[int]()
originalFut := originalPromise.Future()
// Use a background context for OnComplete, as we expect the
// future to complete normally.
ctxComplete := context.Background()
var wg sync.WaitGroup
wg.Add(1)
var (
callbackInvoked atomic.Bool
callbackResultValue fn.Result[int]
mu sync.Mutex
)
// Register an OnComplete callback. This callback will execute
// once the originalFut completes.
originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) {
mu.Lock()
callbackResultValue = res
mu.Unlock()
callbackInvoked.Store(true)
wg.Done()
})
// Complete the original future in a separate goroutine to
// simulate an asynchronous operation.
go func() {
if originalErr != nil {
originalPromise.Complete(
fn.Err[int](originalErr),
)
} else {
originalPromise.Complete(fn.Ok(valToSet))
}
}()
// Use a wait group and a channel to wait for the callback's
// execution.
waitChan := make(chan struct{})
go func() {
wg.Wait()
close(waitChan)
}()
select {
// The callback should be invoked as the future completes.
case <-waitChan:
case <-time.After(50 * time.Millisecond):
require.Fail(
t, "OnComplete callback timed out waiting "+
"for execution",
)
}
require.True(t, callbackInvoked.Load())
mu.Lock()
defer mu.Unlock()
// Verify that the callback received the correct result (either
// the error or the value from the completed future).
if originalErr != nil {
require.True(t, callbackResultValue.IsErr())
require.ErrorIs(
t, callbackResultValue.Err(), originalErr,
"OnComplete with error",
)
} else {
require.False(
t, callbackResultValue.IsErr(),
"OnComplete with value",
)
callbackResultValue.WhenOk(func(val int) {
require.Equal(
t, valToSet, val,
"OnComplete with value",
)
})
}
})
}
// TestCompleteWith verifies that CompleteWith resolves a promise with the
// supplied value, that the resolution is immediately visible on the Future, and
// that a second call is a safe no-op (idempotency inherited from Complete).
func TestCompleteWith(t *testing.T) {
t.Parallel()
// Normal completion — value should be visible on the future.
promise := NewPromise[int]()
CompleteWith(promise, 42)
result := promise.Future().Await(context.Background())
require.False(t, result.IsErr())
result.WhenOk(func(v int) {
require.Equal(t, 42, v)
})
// Second call must be a no-op; the future must still hold 42.
CompleteWith(promise, 99)
result2 := promise.Future().Await(context.Background())
require.False(t, result2.IsErr())
result2.WhenOk(func(v int) {
require.Equal(t, 42, v, "second CompleteWith must not overwrite")
})
}
// TestAwaitFuture verifies that AwaitFuture unpacks a resolved future into a
// (value, nil) pair, that a future completed with fn.Err is reported as a
// (zero, err) pair, and that context cancellation before resolution is
// reported as a (zero, ctx.Err()) pair.
func TestAwaitFuture(t *testing.T) {
t.Parallel()
// Resolved future — should return the value with a nil error.
promise := NewPromise[string]()
CompleteWith(promise, "hello")
val, err := AwaitFuture(context.Background(), promise.Future())
require.NoError(t, err)
require.Equal(t, "hello", val)
// Future completed with fn.Err — should surface the error as the
// second return value with the zero string value.
sentinel := fmt.Errorf("result-level error")
errPromise := NewPromise[string]()
errPromise.Complete(fn.Err[string](sentinel))
val3, err3 := AwaitFuture(context.Background(), errPromise.Future())
require.ErrorIs(t, err3, sentinel)
require.Equal(t, "", val3, "zero value expected on fn.Err result")
// Cancelled context — should return the zero value and ctx.Err().
unresolved := NewPromise[string]()
ctx, cancel := context.WithCancel(context.Background())
cancel()
val2, err2 := AwaitFuture(ctx, unresolved.Future())
require.ErrorIs(t, err2, context.Canceled)
require.Equal(t, "", val2, "zero value expected on cancellation")
}
func TestPromiseCompleteIdempotency(t *testing.T) {
t.Parallel()
promise := NewPromise[string]()
future := promise.Future()
// First completion should succeed.
firstResult := fn.Ok("first-value")
ok := promise.Complete(firstResult)
require.True(t, ok, "first Complete should return true")
// Second completion with a different value should be ignored.
secondResult := fn.Ok("second-value")
ok = promise.Complete(secondResult)
require.False(t, ok, "second Complete should return false")
// Third completion with an error should also be ignored.
thirdResult := fn.Err[string](fmt.Errorf("should be ignored"))
ok = promise.Complete(thirdResult)
require.False(t, ok, "third Complete should return false")
// The future should contain the first value.
result := future.Await(context.Background())
require.False(t, result.IsErr(), "future should not be an error")
result.WhenOk(func(val string) {
require.Equal(
t, "first-value", val,
"future should contain the first completion value",
)
})
}

27
actor/go.mod Normal file
View file

@ -0,0 +1,27 @@
module github.com/lightningnetwork/lnd/actor
go 1.25.11
require (
github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084
github.com/lightningnetwork/lnd/fn/v2 v2.0.8
github.com/lightningnetwork/lnd/queue v1.1.1
github.com/stretchr/testify v1.8.1
pgregory.net/rapid v1.2.0
)
require (
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/lightningnetwork/lnd/queue => ../queue
replace github.com/lightningnetwork/lnd/ticker => ../ticker
replace github.com/lightningnetwork/lnd/fn/v2 => ../fn

27
actor/go.sum Normal file
View file

@ -0,0 +1,27 @@
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0=
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084 h1:y3bvkt8ki0KX35eUEU8XShRHusz1S+55QwXUTmxn888=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=

124
actor/interface.go Normal file
View file

@ -0,0 +1,124 @@
package actor
import (
"context"
"errors"
"fmt"
"github.com/lightningnetwork/lnd/fn/v2"
)
// ErrActorTerminated indicates that an operation failed because the target
// actor was terminated or in the process of shutting down.
var ErrActorTerminated = fmt.Errorf("actor terminated")
// ErrMessageDropped indicates that a message was dropped by the mailbox's
// backpressure mechanism (e.g., RED-style load shedding).
var ErrMessageDropped = errors.New("message dropped by backpressure")
// ErrEmptyActorID is returned when an actor is created with an empty ID.
var ErrEmptyActorID = fmt.Errorf("actor ID must not be empty")
// ErrNilBehavior is returned when an actor is created with a nil behavior.
var ErrNilBehavior = fmt.Errorf("actor behavior must not be nil")
// ErrDuplicateActorID is returned when attempting to register an actor with an
// ID that is already in use within the actor system.
var ErrDuplicateActorID = fmt.Errorf("actor ID already registered")
// BaseMessage is a helper struct that can be embedded in message types defined
// outside the actor package to satisfy the Message interface's unexported
// messageMarker method.
type BaseMessage struct{}
// messageMarker implements the unexported method for the Message interface,
// allowing types that embed BaseMessage to satisfy the Message interface.
func (BaseMessage) messageMarker() {}
// Message is a sealed interface for actor messages. Actors will receive
// messages conforming to this interface. The interface is "sealed" by the
// unexported messageMarker method, meaning only types that can satisfy it
// (e.g., by embedding BaseMessage or being in the same package) can be
// Messages.
type Message interface {
// messageMarker is a private method that makes this a sealed interface
// (see BaseMessage for embedding).
messageMarker()
// MessageType returns the type name of the message for
// routing/filtering.
MessageType() string
}
// Future represents the result of an asynchronous computation. It allows
// consumers to wait for the result (Await), apply transformations upon
// completion (ThenApply), or register a callback to be executed when the
// result is available (OnComplete).
type Future[T any] interface {
// Await blocks until the result is available or the context is
// cancelled, then returns it.
Await(ctx context.Context) fn.Result[T]
// ThenApply registers a function to transform the result of a future.
// The original future is not modified, a new instance of the future is
// returned. If the passed context is cancelled while waiting for the
// original future to complete, the new future will complete with the
// context's error.
ThenApply(ctx context.Context, fn func(T) T) Future[T]
// OnComplete registers a function to be called when the result of the
// future is ready. If the passed context is cancelled before the future
// completes, the callback function will be invoked with the context's
// error.
OnComplete(ctx context.Context, fn func(fn.Result[T]))
}
// Promise is an interface that allows for the completion of an associated
// Future. It provides a way to set the result of an asynchronous operation.
// The producer of an asynchronous result uses a Promise to set the outcome,
// while consumers use the associated Future to retrieve it.
type Promise[T any] interface {
// Future returns the Future interface associated with this Promise.
// Consumers can use this to Await the result or register callbacks.
Future() Future[T]
// Complete attempts to set the result of the future. It returns true if
// this call successfully set the result (i.e., it was the first to
// complete it), and false if the future had already been completed.
Complete(result fn.Result[T]) bool
}
// TellOnlyRef is a reference to an actor that only supports "tell" operations.
// This is useful for scenarios where only fire-and-forget message passing is
// needed, or to restrict capabilities.
type TellOnlyRef[M Message] interface {
// Tell sends a message without waiting for a response. If the
// context is cancelled before the message can be sent to the actor's
// mailbox, the message may be dropped.
Tell(ctx context.Context, msg M)
// ID returns the unique identifier for this actor.
ID() string
}
// ActorRef is a reference to an actor that supports both "tell" and "ask"
// operations. It embeds TellOnlyRef and adds the Ask method for
// request-response interactions.
type ActorRef[M Message, R any] interface {
TellOnlyRef[M]
// Ask sends a message and returns a Future for the response.
// The Future will be completed with the actor's reply or an error
// if the operation fails (e.g., context cancellation before send).
Ask(ctx context.Context, msg M) Future[R]
}
// ActorBehavior defines the logic for how an actor processes incoming messages.
// It is a strategy interface that encapsulates the actor's reaction to
// messages.
type ActorBehavior[M Message, R any] interface {
// Receive processes a message and returns a Result. The provided
// context is the actor's internal context, which can be used to
// detect actor shutdown requests.
Receive(actorCtx context.Context, msg M) fn.Result[R]
}

12
actor/log.go Normal file
View file

@ -0,0 +1,12 @@
package actor
import "github.com/btcsuite/btclog/v2"
// log is a logger that is initialized as disabled. This means the package will
// not perform any logging by default until a logger is set.
var log = btclog.Disabled
// UseLogger uses a specified Logger to output package logging info.
func UseLogger(logger btclog.Logger) {
log = logger
}

176
actor/mailbox.go Normal file
View file

@ -0,0 +1,176 @@
package actor
import (
"context"
"iter"
"sync"
"sync/atomic"
)
// Mailbox represents the message queue for an actor. It provides methods for
// sending messages and receiving them via an iterator pattern.
type Mailbox[M Message, R any] interface {
// Send attempts to send an envelope to the mailbox with context-based
// cancellation. Returns true if sent successfully, false if the
// context was cancelled or the mailbox is closed.
Send(ctx context.Context, env envelope[M, R]) bool
// TrySend attempts to send without blocking. Returns true if the
// envelope was sent, false if the mailbox is full or closed.
TrySend(env envelope[M, R]) bool
// Receive returns an iterator for consuming messages from the mailbox.
// The iterator will yield messages until the mailbox is closed or the
// context is cancelled.
Receive(ctx context.Context) iter.Seq[envelope[M, R]]
// Close closes the mailbox, preventing new messages from being sent.
// Any remaining messages can still be consumed via Receive.
Close()
// IsClosed returns true if the mailbox has been closed.
IsClosed() bool
// Drain returns an iterator that yields all remaining messages in the
// mailbox after it has been closed. This is useful for cleanup.
Drain() iter.Seq[envelope[M, R]]
}
// ChannelMailbox is a channel-based implementation of the Mailbox interface.
type ChannelMailbox[M Message, R any] struct {
ch chan envelope[M, R]
closed atomic.Bool
// mu protects Send/TrySend operations to prevent send-on-closed-channel
// panics. Close() acquires write lock, Send/TrySend acquire read lock.
mu sync.RWMutex
// closeOnce ensures Close() executes exactly once.
closeOnce sync.Once
// actorCtx is the actor's context for lifecycle management.
actorCtx context.Context
}
// NewChannelMailbox creates a new channel-based mailbox with the specified
// buffer capacity and actor context.
func NewChannelMailbox[M Message, R any](actorCtx context.Context,
capacity int) *ChannelMailbox[M, R] {
if capacity <= 0 {
capacity = 1
}
return &ChannelMailbox[M, R]{
ch: make(chan envelope[M, R], capacity),
actorCtx: actorCtx,
}
}
// Send implements Mailbox.Send with context-aware blocking send.
func (m *ChannelMailbox[M, R]) Send(ctx context.Context,
env envelope[M, R]) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if m.IsClosed() {
return false
}
select {
case m.ch <- env:
return true
case <-ctx.Done():
return false
case <-m.actorCtx.Done():
// Actor is shutting down.
return false
}
}
// TrySend implements Mailbox.TrySend with non-blocking send.
func (m *ChannelMailbox[M, R]) TrySend(env envelope[M, R]) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if m.IsClosed() {
return false
}
select {
case m.ch <- env:
return true
default:
return false
}
}
// Receive implements Mailbox.Receive using iter.Seq pattern.
func (m *ChannelMailbox[M, R]) Receive(
ctx context.Context) iter.Seq[envelope[M, R]] {
return func(yield func(envelope[M, R]) bool) {
for {
select {
case env, ok := <-m.ch:
if !ok {
return
}
if !yield(env) {
return
}
case <-ctx.Done():
return
case <-m.actorCtx.Done():
return
}
}
}
}
// Close implements Mailbox.Close.
func (m *ChannelMailbox[M, R]) Close() {
m.closeOnce.Do(func() {
m.mu.Lock()
defer m.mu.Unlock()
m.closed.Store(true)
close(m.ch)
})
}
// IsClosed implements Mailbox.IsClosed.
func (m *ChannelMailbox[M, R]) IsClosed() bool {
return m.closed.Load()
}
// Drain implements Mailbox.Drain for cleanup after close.
func (m *ChannelMailbox[M, R]) Drain() iter.Seq[envelope[M, R]] {
return func(yield func(envelope[M, R]) bool) {
// Only drain if closed.
if !m.IsClosed() {
return
}
// Drain all remaining messages from the channel.
for {
select {
case env, ok := <-m.ch:
// Channel closed, nothing left to drain.
if !ok {
return
}
if !yield(env) {
return
}
default:
// Channel empty, done draining.
return
}
}
}
}

593
actor/mailbox_test.go Normal file
View file

@ -0,0 +1,593 @@
package actor
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
// TestMessage is a test message type that embeds BaseMessage.
type TestMessage struct {
BaseMessage
Value int
}
// MessageType returns the type name of the message for routing/filtering.
func (tm TestMessage) MessageType() string {
return "TestMessage"
}
// TestChannelMailboxSend tests the Send method of ChannelMailbox.
func TestChannelMailboxSend(t *testing.T) {
t.Run("successful send", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
ctx := context.Background()
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
sent := mailbox.Send(ctx, env)
require.True(t, sent, "Send should succeed")
})
t.Run("send with cancelled context", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 1)
// Fill the mailbox first.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
mailbox.TrySend(env)
ctx, cancel := context.WithCancel(context.Background())
// Cancel immediately.
cancel()
env2 := envelope[TestMessage, int]{
message: TestMessage{Value: 43},
promise: nil,
}
sent := mailbox.Send(ctx, env2)
require.False(t, sent, "Send should fail with cancelled context")
})
t.Run("send to closed mailbox", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
mailbox.Close()
ctx := context.Background()
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
sent := mailbox.Send(ctx, env)
require.False(t, sent, "Send should fail on closed mailbox")
})
}
// TestChannelMailboxTrySend tests the TrySend method of ChannelMailbox.
func TestChannelMailboxTrySend(t *testing.T) {
t.Run("successful try send", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
sent := mailbox.TrySend(env)
require.True(t, sent, "TrySend should succeed")
})
t.Run("try send to full mailbox", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 1)
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
// Fill the mailbox.
sent := mailbox.TrySend(env)
require.True(t, sent, "First TrySend should succeed")
// Try to send again - should fail.
sent = mailbox.TrySend(env)
require.False(t, sent, "TrySend should fail on full mailbox")
})
t.Run("try send to closed mailbox", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
mailbox.Close()
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
sent := mailbox.TrySend(env)
require.False(t, sent, "TrySend should fail on closed mailbox")
})
}
// TestChannelMailboxReceive tests the Receive method of ChannelMailbox.
func TestChannelMailboxReceive(t *testing.T) {
t.Run("receive messages", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
ctx := context.Background()
// Send some messages.
for i := 0; i < 3; i++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
promise: nil,
}
mailbox.Send(ctx, env)
}
// Start receiving in a goroutine.
var received []int
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for env := range mailbox.Receive(ctx) {
received = append(received, env.message.Value)
}
}()
// Close the mailbox after sending all messages.
mailbox.Close()
wg.Wait()
require.Len(t, received, 3, "Should receive 3 messages")
require.Equal(t, []int{0, 1, 2}, received, "Should receive messages in order")
})
t.Run("receive with cancelled context", func(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
ctx, cancel := context.WithCancel(context.Background())
// Send a message.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
mailbox.Send(context.Background(), env)
// Start receiving.
var received int
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for env := range mailbox.Receive(ctx) {
received++
_ = env
}
}()
// Cancel the context.
cancel()
wg.Wait()
// Might receive 0 or 1 message depending on timing.
require.LessOrEqual(t, received, 1,
"Should stop receiving after context cancel")
})
}
// TestChannelMailboxClose tests the Close and IsClosed methods.
func TestChannelMailboxClose(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
require.False(t, mailbox.IsClosed(), "Mailbox should not be closed initially")
mailbox.Close()
require.True(t, mailbox.IsClosed(), "Mailbox should be closed after Close()")
// Closing again should be safe.
mailbox.Close()
require.True(t, mailbox.IsClosed(), "Mailbox should remain closed")
}
// TestChannelMailboxDrain tests the Drain method of ChannelMailbox.
func TestChannelMailboxDrain(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
ctx := context.Background()
// Send some messages.
for i := 0; i < 3; i++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: i},
promise: nil,
}
mailbox.Send(ctx, env)
}
// Close the mailbox.
mailbox.Close()
// Drain messages.
var drained []int
for env := range mailbox.Drain() {
drained = append(drained, env.message.Value)
}
require.Len(t, drained, 3, "Should drain 3 messages")
require.Equal(t, []int{0, 1, 2}, drained, "Should drain messages in order")
}
// TestChannelMailboxConcurrent tests concurrent operations on ChannelMailbox.
func TestChannelMailboxConcurrent(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 100)
ctx := context.Background()
const numSenders = 10
const messagesPerSender = 100
var wg sync.WaitGroup
// Start multiple senders.
for i := 0; i < numSenders; i++ {
wg.Add(1)
go func(senderID int) {
defer wg.Done()
for j := 0; j < messagesPerSender; j++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: senderID*1000 + j},
promise: nil,
}
mailbox.Send(ctx, env)
}
}(i)
}
// Start receiver.
received := make([]int, 0, numSenders*messagesPerSender)
var receiverWg sync.WaitGroup
receiverWg.Add(1)
go func() {
defer receiverWg.Done()
for env := range mailbox.Receive(ctx) {
received = append(received, env.message.Value)
}
}()
// Wait for all senders to complete.
wg.Wait()
// Close the mailbox now that all sends are complete.
mailbox.Close()
receiverWg.Wait()
require.Len(t, received, numSenders*messagesPerSender,
"Should receive all messages")
}
// TestChannelMailboxZeroCapacity tests that zero capacity defaults to 1.
func TestChannelMailboxZeroCapacity(t *testing.T) {
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 0)
// Should default to capacity of 1.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
sent := mailbox.TrySend(env)
require.True(t, sent, "Should be able to send one message")
// Second send should fail (mailbox full).
sent = mailbox.TrySend(env)
require.False(t, sent, "Second send should fail on full mailbox")
}
// TestChannelMailboxActorContext tests that the mailbox respects the actor's
// context for cancellation.
func TestChannelMailboxActorContext(t *testing.T) {
t.Run("send respects actor context", func(t *testing.T) {
actorCtx, actorCancel := context.WithCancel(context.Background())
mailbox := NewChannelMailbox[TestMessage, int](actorCtx, 1)
// Fill the mailbox.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
mailbox.TrySend(env)
// Cancel the actor context.
actorCancel()
// Try to send with a fresh caller context - should fail due to
// actor context cancellation.
callerCtx := context.Background()
env2 := envelope[TestMessage, int]{
message: TestMessage{Value: 43},
promise: nil,
}
sent := mailbox.Send(callerCtx, env2)
require.False(t, sent, "Send should fail when actor context is cancelled")
})
t.Run("receive respects actor context", func(t *testing.T) {
actorCtx, actorCancel := context.WithCancel(context.Background())
mailbox := NewChannelMailbox[TestMessage, int](actorCtx, 10)
// Send a message.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 42},
promise: nil,
}
mailbox.Send(context.Background(), env)
// Start receiving with a fresh context.
callerCtx := context.Background()
var received int
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for env := range mailbox.Receive(callerCtx) {
received++
_ = env
}
}()
// Cancel the actor context.
actorCancel()
wg.Wait()
// Should have stopped receiving due to actor context cancellation.
require.LessOrEqual(t, received, 1,
"Should stop receiving when actor context is cancelled")
})
}
// TestMailboxConcurrentSendAndClose tests concurrent Send and Close operations
// to ensure no race conditions or panics occur.
func TestMailboxConcurrentSendAndClose(t *testing.T) {
const numSenders = 20
const sendsPerSender = 100
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 100)
ctx := context.Background()
var wg sync.WaitGroup
// Start receiver to drain messages.
var recvWg sync.WaitGroup
recvWg.Add(1)
go func() {
defer recvWg.Done()
for range mailbox.Receive(ctx) {
// Just drain.
}
}()
// Start multiple senders.
for i := 0; i < numSenders; i++ {
wg.Add(1)
go func(senderID int) {
defer wg.Done()
for j := 0; j < sendsPerSender; j++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: senderID*1000 + j},
promise: nil,
}
// Send may fail if mailbox closes, that's ok.
mailbox.Send(ctx, env)
}
}(i)
}
// Concurrently close the mailbox multiple times from different
// goroutines.
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mailbox.Close()
}()
}
wg.Wait()
recvWg.Wait()
// Mailbox should be closed.
require.True(t, mailbox.IsClosed(), "Mailbox should be closed")
// Further sends should fail without panic.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 999},
promise: nil,
}
sent := mailbox.Send(ctx, env)
require.False(t, sent, "Send should fail on closed mailbox")
}
// TestMailboxConcurrentTrySendAndClose tests concurrent TrySend and Close
// operations to ensure no race conditions or panics occur.
func TestMailboxConcurrentTrySendAndClose(t *testing.T) {
const numSenders = 20
const sendsPerSender = 100
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
var wg sync.WaitGroup
// Start multiple senders using TrySend.
for i := 0; i < numSenders; i++ {
wg.Add(1)
go func(senderID int) {
defer wg.Done()
for j := 0; j < sendsPerSender; j++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: senderID*1000 + j},
promise: nil,
}
// TrySend may fail if mailbox is full or closed.
mailbox.TrySend(env)
}
}(i)
}
// Concurrently close the mailbox.
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mailbox.Close()
}()
}
wg.Wait()
// Mailbox should be closed.
require.True(t, mailbox.IsClosed(), "Mailbox should be closed")
// Further sends should fail without panic.
env := envelope[TestMessage, int]{
message: TestMessage{Value: 999},
promise: nil,
}
sent := mailbox.TrySend(env)
require.False(t, sent, "TrySend should fail on closed mailbox")
}
// TestMailboxMultipleCloseCallers tests that multiple goroutines calling
// Close() simultaneously don't cause panics or issues.
func TestMailboxMultipleCloseCallers(t *testing.T) {
const numClosers = 100
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 10)
var wg sync.WaitGroup
// Start many goroutines all trying to close the mailbox.
for i := 0; i < numClosers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mailbox.Close()
}()
}
wg.Wait()
// Mailbox should be closed exactly once.
require.True(t, mailbox.IsClosed(), "Mailbox should be closed")
// Calling Close again should be safe.
mailbox.Close()
require.True(t, mailbox.IsClosed(), "Mailbox should remain closed")
}
// TestMailboxCloseWhileSending tests closing the mailbox while multiple
// senders are actively sending messages.
func TestMailboxCloseWhileSending(t *testing.T) {
const numSenders = 10
const sendsPerSender = 1000
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 100)
ctx := context.Background()
var sendWg sync.WaitGroup
// Start multiple senders.
for i := 0; i < numSenders; i++ {
sendWg.Add(1)
go func(senderID int) {
defer sendWg.Done()
for j := 0; j < sendsPerSender; j++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: senderID*1000 + j},
promise: nil,
}
// Send may fail after close, that's expected.
mailbox.Send(ctx, env)
}
}(i)
}
// Start receiver to drain messages.
var recvWg sync.WaitGroup
recvWg.Add(1)
receivedCount := 0
go func() {
defer recvWg.Done()
for range mailbox.Receive(ctx) {
receivedCount++
}
}()
// Close mailbox while sends are happening.
mailbox.Close()
sendWg.Wait()
recvWg.Wait()
// Should have received at least some messages (exact count depends on
// timing).
t.Logf("Received %d messages before close", receivedCount)
// Mailbox should be closed.
require.True(t, mailbox.IsClosed(), "Mailbox should be closed")
}
// TestMailboxStressTest performs a high-concurrency stress test with multiple
// senders, receivers, and close operations.
func TestMailboxStressTest(t *testing.T) {
const numSenders = 50
const numReceivers = 5
const sendsPerSender = 200
mailbox := NewChannelMailbox[TestMessage, int](context.Background(), 200)
ctx := context.Background()
var sendWg sync.WaitGroup
// Start multiple senders.
for i := 0; i < numSenders; i++ {
sendWg.Add(1)
go func(senderID int) {
defer sendWg.Done()
for j := 0; j < sendsPerSender; j++ {
env := envelope[TestMessage, int]{
message: TestMessage{Value: senderID*1000 + j},
promise: nil,
}
mailbox.Send(ctx, env)
}
}(i)
}
// Start multiple receivers.
var recvWg sync.WaitGroup
for i := 0; i < numReceivers; i++ {
recvWg.Add(1)
go func() {
defer recvWg.Done()
for range mailbox.Receive(ctx) {
// Just drain messages.
}
}()
}
// Wait for all sends to complete.
sendWg.Wait()
// Close mailbox.
mailbox.Close()
// Wait for all receivers to finish.
recvWg.Wait()
// Mailbox should be closed.
require.True(t, mailbox.IsClosed(), "Mailbox should be closed")
}

154
actor/router.go Normal file
View file

@ -0,0 +1,154 @@
package actor
import (
"context"
"errors"
"sync/atomic"
"github.com/lightningnetwork/lnd/fn/v2"
)
// ErrNoActorsAvailable is returned when a router cannot find any actors
// registered for its service key to forward a message to.
var ErrNoActorsAvailable = errors.New("no actors available for service key")
// Compile-time assertion that Router satisfies the ActorRef interface.
var _ ActorRef[Message, any] = (*Router[Message, any])(nil)
// RoutingStrategy defines the interface for selecting an actor from a list of
// available actors.
// The M (Message) and R (Response) type parameters ensure that the strategy
// is compatible with the types of actors it will be selecting.
type RoutingStrategy[M Message, R any] interface {
// Select chooses an ActorRef from the provided slice. It returns the
// selected actor or an error if no actor can be selected (e.g., if the
// list is empty or another strategy-specific issue occurs).
Select(refs []ActorRef[M, R]) (ActorRef[M, R], error)
}
// RoundRobinStrategy implements a round-robin selection strategy. It is generic
// over M and R to match the RoutingStrategy interface, though its logic doesn't
// depend on these types directly for the selection mechanism itself.
type RoundRobinStrategy[M Message, R any] struct {
// index is used to pick the next actor in a round-robin fashion. It
// must be accessed atomically to ensure thread-safety if multiple
// goroutines use the same strategy instance (which they will via the
// router).
index uint64
}
// NewRoundRobinStrategy creates a new RoundRobinStrategy, initialized for
// round-robin selection.
func NewRoundRobinStrategy[M Message, R any]() *RoundRobinStrategy[M, R] {
return &RoundRobinStrategy[M, R]{}
}
// Select picks an actor from the list using a round-robin algorithm.
func (s *RoundRobinStrategy[M, R]) Select(
refs []ActorRef[M, R],
) (ActorRef[M, R], error) {
if len(refs) == 0 {
return nil, ErrNoActorsAvailable
}
// Atomically increment and get the current index for selection.
// We subtract 1 because AddUint64 returns the new value (which is
// 1-based for the first call after initialization to 0), and slice
// indexing is 0-based.
idx := atomic.AddUint64(&s.index, 1) - 1
selectedRef := refs[idx%uint64(len(refs))]
return selectedRef, nil
}
// Router is a message-dispatching component that fronts multiple actors
// registered under a specific ServiceKey. It uses a RoutingStrategy to
// distribute messages to one of the available actors. It is generic over M
// (Message type) and R (Response type) to match the actors it routes to.
type Router[M Message, R any] struct {
receptionist *Receptionist
serviceKey ServiceKey[M, R]
strategy RoutingStrategy[M, R]
dlo ActorRef[Message, any] // Dead Letter Office reference.
}
// NewRouter creates a new Router for a given service key and strategy. The
// receptionist is used to discover actors registered with the service key.
// The router itself is not an actor but a message dispatcher that behaves like
// an ActorRef from the sender's perspective.
func NewRouter[M Message, R any](receptionist *Receptionist,
key ServiceKey[M, R], strategy RoutingStrategy[M, R],
dlo ActorRef[Message, any]) *Router[M, R] {
return &Router[M, R]{
receptionist: receptionist,
serviceKey: key,
strategy: strategy,
dlo: dlo,
}
}
// getActor dynamically finds available actors for the service key and selects
// one using the configured strategy. This method is called internally by Tell
// and Ask on each invocation to ensure up-to-date actor discovery.
func (r *Router[M, R]) getActor() (ActorRef[M, R], error) {
// Discover available actors from the receptionist.
availableActors := FindInReceptionist(r.receptionist, r.serviceKey)
if len(availableActors) == 0 {
return nil, ErrNoActorsAvailable
}
// Select one actor using the strategy.
return r.strategy.Select(availableActors)
}
// Tell sends a message to one of the actors managed by the router, selected by
// the routing strategy. If no actors are available or the send context is
// cancelled before the message can be enqueued in the target actor's mailbox,
// the message may be dropped. Errors during actor selection (e.g.,
// ErrNoActorsAvailable) are currently not propagated from Tell, aligning with
// its fire-and-forget nature. Such errors could be logged internally if needed.
func (r *Router[M, R]) Tell(ctx context.Context, msg M) {
selectedActor, err := r.getActor()
if err != nil {
// If no actors are available for the service, and a DLO is
// configured, forward the message there.
if errors.Is(err, ErrNoActorsAvailable) && r.dlo != nil {
r.dlo.Tell(context.Background(), msg)
} else {
log.Warnf("Router(%s): message %s dropped "+
"(no actors available, no DLO configured)",
r.serviceKey.name, msg.MessageType())
}
return
}
selectedActor.Tell(ctx, msg)
}
// Ask sends a message to one of the actors managed by the router, selected by
// the routing strategy, and returns a Future for the response. If no actors are
// available (ErrNoActorsAvailable), the Future will be completed with this
// error. If the send context is cancelled before the message can be enqueued in
// the chosen actor's mailbox, the Future will be completed with the context's
// error.
func (r *Router[M, R]) Ask(ctx context.Context, msg M) Future[R] {
selectedActor, err := r.getActor()
if err != nil {
// If no actor could be selected (e.g., none available),
// complete the promise immediately with the selection error.
promise := NewPromise[R]()
promise.Complete(fn.Err[R](err))
return promise.Future()
}
return selectedActor.Ask(ctx, msg)
}
// ID provides an identifier for the router. Since a router isn't an actor
// itself but a dispatcher for a service, its ID can be based on the service
// key.
func (r *Router[M, R]) ID() string {
return "router(" + r.serviceKey.name + ")"
}

447
actor/system.go Normal file
View file

@ -0,0 +1,447 @@
package actor
import (
"context"
"errors"
"fmt"
"sync"
"github.com/lightningnetwork/lnd/fn/v2"
)
// stoppable defines an interface for components that can be stopped.
// This is unexported as it's an internal detail of ActorSystem for managing
// actors that need to be shut down.
type stoppable interface {
Stop()
}
// SystemConfig holds configuration parameters for the ActorSystem.
type SystemConfig struct {
// MailboxCapacity is the default capacity for actor mailboxes.
MailboxCapacity int
}
// DefaultConfig returns a default configuration for the ActorSystem.
// The default mailbox capacity of 100 means each actor can buffer up to 100
// pending messages (envelopes). Each envelope holds a message and an optional
// promise pointer, so the memory overhead per actor is roughly proportional to
// the size of the messages being sent multiplied by this capacity.
func DefaultConfig() SystemConfig {
return SystemConfig{
MailboxCapacity: 100,
}
}
// ActorSystem manages the lifecycle of actors and provides coordination
// services such as a receptionist for actor discovery and a dead letter office
// for undeliverable messages. It also handles the graceful shutdown of all
// managed actors.
type ActorSystem struct {
// receptionist is used for actor discovery.
receptionist *Receptionist
// actors stores all actors managed by the system, keyed by their ID.
// This includes the deadLetterActor.
actors map[string]stoppable
// deadLetterActor handles undeliverable messages.
deadLetterActor ActorRef[Message, any]
// config holds the system-wide configuration.
config SystemConfig
// mu protects the 'actors' map.
mu sync.RWMutex
// ctx is the main context for the actor system.
ctx context.Context
// cancel cancels the main system context.
cancel context.CancelFunc
}
// NewActorSystem creates a new actor system using the default configuration.
func NewActorSystem() *ActorSystem {
return NewActorSystemWithConfig(DefaultConfig())
}
// NewActorSystemWithConfig creates a new actor system with custom configuration
func NewActorSystemWithConfig(config SystemConfig) *ActorSystem {
ctx, cancel := context.WithCancel(context.Background())
// Initialize the core ActorSystem components.
system := &ActorSystem{
receptionist: newReceptionist(),
config: config,
actors: make(map[string]stoppable),
ctx: ctx,
cancel: cancel,
}
// Define the behavior for the dead letter actor. It logs undeliverable
// messages and returns an error.
deadLetterBehavior := NewFunctionBehavior(
func(ctx context.Context, msg Message) fn.Result[any] {
log.Warnf("Dead letter received: message type=%s",
msg.MessageType())
return fn.Err[any](errors.New(
"message undeliverable: " + msg.MessageType(),
))
},
)
// Create the raw dead letter actor (*Actor instance). The DLO's own DLO
// reference is nil to prevent loops if messages to the DLO itself fail.
deadLetterActorCfg := ActorConfig[Message, any]{
ID: "dead-letters",
Behavior: deadLetterBehavior,
DLO: nil,
MailboxSize: config.MailboxCapacity,
}
deadLetterRawActor, err := NewActor[Message, any](deadLetterActorCfg)
if err != nil {
// This should never happen since we control the DLO config.
panic("failed to create dead letter actor: " + err.Error())
}
deadLetterRawActor.Start()
system.deadLetterActor = deadLetterRawActor.Ref()
// Add the raw actor to the map of stoppable actors. No lock needed here
// as 'system' is not yet accessible concurrently.
system.actors[deadLetterRawActor.id] = deadLetterRawActor
// The system is now fully initialized and ready.
return system
}
// ActorOption is a functional option for customizing actor creation.
type ActorOption[M Message, R any] func(*ActorConfig[M, R])
// WithMailboxFactory returns an ActorOption that sets a custom mailbox factory.
func WithMailboxFactory[M Message, R any](
f MailboxFactory[M, R]) ActorOption[M, R] {
return func(cfg *ActorConfig[M, R]) {
cfg.MailboxFactory = f
}
}
// WithMailboxSize returns an ActorOption that overrides the default mailbox
// size.
func WithMailboxSize[M Message, R any](size int) ActorOption[M, R] {
return func(cfg *ActorConfig[M, R]) {
cfg.MailboxSize = size
}
}
// RegisterWithSystem creates an actor with the given ID, service key, and
// behavior within the specified ActorSystem. It starts the actor, adds it to
// the system's management, registers it with the receptionist using the
// provided key, and returns its ActorRef.
func RegisterWithSystem[M Message, R any](as *ActorSystem, id string,
key ServiceKey[M, R],
behavior ActorBehavior[M, R],
opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
actorCfg := ActorConfig[M, R]{
ID: id,
Behavior: behavior,
DLO: as.deadLetterActor,
MailboxSize: as.config.MailboxCapacity,
}
for _, opt := range opts {
opt(&actorCfg)
}
// Check for duplicate actor ID before creating the actor.
as.mu.Lock()
if _, exists := as.actors[id]; exists {
as.mu.Unlock()
return nil, fmt.Errorf("%w: %s", ErrDuplicateActorID, id)
}
actorInstance, err := NewActor(actorCfg)
if err != nil {
as.mu.Unlock()
return nil, err
}
actorInstance.Start()
// Add the actor instance to the system's list of stoppable actors.
as.actors[actorInstance.id] = actorInstance
as.mu.Unlock()
log.Infof("ActorSystem: registered actor %s with service key %s",
id, key.name)
// Register the actor's reference with the receptionist under the given
// service key, making it discoverable by other parts of the system.
RegisterWithReceptionist(as.receptionist, key, actorInstance.Ref())
return actorInstance.Ref(), nil
}
// Receptionist returns the system's receptionist, which can be used for
// actor service discovery (finding actors by ServiceKey).
func (as *ActorSystem) Receptionist() *Receptionist {
return as.receptionist
}
// DeadLetters returns a reference to the system's dead letter actor. Messages
// that cannot be delivered to their intended recipient (e.g., if an Ask
// context is cancelled before enqueuing) may be routed here if not otherwise
// handled.
func (as *ActorSystem) DeadLetters() ActorRef[Message, any] {
return as.deadLetterActor
}
// Shutdown gracefully stops the actor system. It iterates through all managed
// actors, including the dead letter actor, and calls their Stop method.
// After initiating the stop for all actors, it cancels the main system context.
// This method is safe for concurrent use.
func (as *ActorSystem) Shutdown() error {
log.Infof("ActorSystem: initiating shutdown")
// Create a slice of actors to stop. This avoids holding the lock while
// calling Stop() on each actor, and includes the dead letter actor.
var actorsToStop []stoppable
as.mu.RLock()
for _, actor := range as.actors {
actorsToStop = append(actorsToStop, actor)
}
as.mu.RUnlock()
// Notify all managed actors to stop. Actor.Stop() is non-blocking.
// Each actor's Stop method will cancel its internal context, leading
// to the termination of its processing goroutine.
for _, actor := range actorsToStop {
actor.Stop()
}
// Clear the actors map after initiating their shutdown.
as.mu.Lock()
as.actors = nil
as.mu.Unlock()
// Finally cancel the main context
// This signals to any other components observing the system's context
// that shutdown has been initiated.
as.cancel()
return nil
}
// StopAndRemoveActor stops a specific actor by its ID and removes it from the
// ActorSystem's management. It returns true if the actor was found and stopped,
// false otherwise.
func (as *ActorSystem) StopAndRemoveActor(id string) bool {
as.mu.Lock()
defer as.mu.Unlock()
actorToStop, exists := as.actors[id]
if !exists {
return false
}
// Stop the actor. This is non-blocking.
actorToStop.Stop()
// Remove from the system's management.
delete(as.actors, id)
return true
}
// UnregisterFromReceptionist removes an actor reference from a service key in
// the given receptionist. It returns true if the reference was found and
// removed, and false otherwise. This is a package-level generic function
// because methods cannot have their own type parameters in Go.
func UnregisterFromReceptionist[M Message, R any](r *Receptionist,
key ServiceKey[M, R], refToRemove ActorRef[M, R]) bool {
r.mu.Lock()
defer r.mu.Unlock()
refs, exists := r.registrations[key.name]
if !exists {
return false
}
found := false
// Build a new slice containing only the references that are not the one
// to be removed.
newRefs := make([]any, 0, max(0, len(refs)-1))
for _, itemInSlice := range refs {
// Try to assert the item from the slice to the specific
// ActorRef[M,R] type we are trying to remove.
if specificActorRef, ok := itemInSlice.(ActorRef[M, R]); ok {
// If the type assertion is successful and it's the one
// we want to remove, mark as found and skip adding it
// to newRefs.
if specificActorRef == refToRemove {
found = true
continue
}
}
newRefs = append(newRefs, itemInSlice)
}
if !found {
return false
}
// If the new list of references is empty, remove the key from the map.
// Otherwise, update the map with the new slice.
if len(newRefs) == 0 {
delete(r.registrations, key.name)
} else {
r.registrations[key.name] = newRefs
}
return true
}
// ServiceKey is a type-safe identifier used for registering and discovering
// actors via the Receptionist. The generic type parameters M (Message) and R
// (Response) ensure that only actors handling compatible message/response types
// are associated with and retrieved for this key.
type ServiceKey[M Message, R any] struct {
name string
}
// NewServiceKey creates a new service key with the given name. The name is used
// as the lookup key within the Receptionist.
func NewServiceKey[M Message, R any](name string) ServiceKey[M, R] {
return ServiceKey[M, R]{name: name}
}
// Spawn registers an actor for this service key within the given ActorSystem.
// It's a convenience method that calls RegisterWithSystem, starting the actor
// and registering it with the receptionist.
func (sk ServiceKey[M, R]) Spawn(as *ActorSystem, id string,
behavior ActorBehavior[M, R],
opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
return RegisterWithSystem(as, id, sk, behavior, opts...)
}
// Unregister removes an actor reference associated with this service key from
// the ActorSystem's receptionist and also stops the actor.
// It returns true if the actor was successfully unregistered from the
// receptionist AND successfully stopped and removed from the system's
// management. Otherwise, it returns false.
func (sk ServiceKey[M, R]) Unregister(as *ActorSystem,
refToRemove ActorRef[M, R]) bool {
unregisteredFromReceptionist := UnregisterFromReceptionist(
as.Receptionist(), sk, refToRemove,
)
// If not found in receptionist, no need to try stopping.
if !unregisteredFromReceptionist {
return false
}
// Attempt to stop and remove the actor from the system.
stoppedAndRemoved := as.StopAndRemoveActor(refToRemove.ID())
return unregisteredFromReceptionist && stoppedAndRemoved
}
// UnregisterAll finds all actor references associated with this service key in
// the ActorSystem's receptionist. For each found actor, it attempts to stop it
// and remove it from system management, and also unregisters it from the
// receptionist.
func (sk ServiceKey[M, R]) UnregisterAll(as *ActorSystem) int {
// First find all the refs that match this service key.
refsFound := FindInReceptionist(as.Receptionist(), sk)
actorsStoppedCount := 0
for _, ref := range refsFound {
// Attempt to stop and remove the actor from the system's active
// management. This is the primary action to deactivate the
// actor. If StopAndRemoveActor returns true, it means an active
// actor was found in the system's `actors` map and was stopped.
if as.StopAndRemoveActor(ref.ID()) {
actorsStoppedCount++
}
// Regardless of whether the actor was actively managed by the
// system (i.e., found in as.actors), attempt to unregister its
// reference from the receptionist. This helps clean up any
// potentially stale entries in the receptionist if an actor was
// removed from the system's management without also being
// unregistered from the receptionist.
UnregisterFromReceptionist(as.Receptionist(), sk, ref)
}
return actorsStoppedCount
}
// Receptionist provides service discovery for actors. Actors can be registered
// under a ServiceKey and later discovered by other actors or system components.
type Receptionist struct {
// registrations stores ActorRef instances, keyed by ServiceKey.name.
registrations map[string][]any
// mu protects access to registrations.
mu sync.RWMutex
}
// newReceptionist creates a new Receptionist instance.
func newReceptionist() *Receptionist {
return &Receptionist{
registrations: make(map[string][]any),
}
}
// RegisterWithReceptionist registers an actor with a service key in the given
// receptionist. This is a package-level generic function because methods
// cannot have their own type parameters in Go (as of the current version).
// It appends the actor reference to the list associated with the key's name.
func RegisterWithReceptionist[M Message, R any](r *Receptionist,
key ServiceKey[M, R], ref ActorRef[M, R]) {
r.mu.Lock()
defer r.mu.Unlock()
// Initialize the slice for this key if it's the first registration.
if _, exists := r.registrations[key.name]; !exists {
r.registrations[key.name] = make([]any, 0)
}
r.registrations[key.name] = append(r.registrations[key.name], ref)
}
// FindInReceptionist returns all actors registered with a service key in the
// given receptionist. This is a package-level generic function because methods
// cannot have their own type parameters. It performs a type assertion to ensure
// that only ActorRefs matching the ServiceKey's generic types (M, R) are
// returned, providing type safety.
func FindInReceptionist[M Message, R any](r *Receptionist,
key ServiceKey[M, R]) []ActorRef[M, R] {
r.mu.RLock()
defer r.mu.RUnlock()
if refs, exists := r.registrations[key.name]; exists {
typedRefs := make([]ActorRef[M, R], 0, len(refs))
for _, ref := range refs {
// Make sure that the reference is of the correct type.
// This type assertion is crucial for type safety, ensuring
// that the returned ActorRefs match the expected M and R.
if typedRef, ok := ref.(ActorRef[M, R]); ok {
typedRefs = append(typedRefs, typedRef)
}
}
return typedRefs
}
return nil
}

958
actor/system_test.go Normal file
View file

@ -0,0 +1,958 @@
package actor
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/require"
)
// TestActorSystemNewActorSystem verifies the basic initialization of an
// ActorSystem, including its default DLO.
func TestActorSystemNewActorSystem(t *testing.T) {
t.Parallel()
as := NewActorSystem()
require.NotNil(t, as, "newActorSystem should not return nil")
require.NotNil(t, as.Receptionist(), "receptionist should not be nil")
require.NotNil(t, as.DeadLetters(), "deadLetters should not be nil")
require.Equal(t, "dead-letters", as.DeadLetters().ID(), "dLO ID mismatch")
// Test the DLO's behavior (it should return an error for Ask).
testDLOMsg := newTestMsg("to-dlo")
future := as.DeadLetters().Ask(context.Background(), testDLOMsg)
result := future.Await(context.Background())
// We should get back an error for asks.
require.True(
t, result.IsErr(), "system DLO should return an error on Ask",
)
expectedErrStr := "message undeliverable: " + testDLOMsg.MessageType()
require.EqualError(
t, result.Err(), expectedErrStr, "dLO error message mismatch",
)
// Shutdown the system to clean up resources.
err := as.Shutdown()
require.NoError(t, err, "actorSystem shutdown failed")
}
// TestActorSystemRegisterWithSystem verifies actor registration, lifecycle
// management within the system.
func TestActorSystemRegisterWithSystem(t *testing.T) {
t.Parallel()
as := NewActorSystem()
defer func() {
err := as.Shutdown()
require.NoError(t, err)
}()
actorID := "test-actor-sys-reg"
serviceKey := NewServiceKey[*testMsg, string]("test-service")
// Using echoBehavior from actor_test.go (implicitly available)
beh := newEchoBehavior(t, 0)
// We'll start off by registering the actor.
actorRef, err := RegisterWithSystem(as, actorID, serviceKey, beh)
require.NoError(t, err)
require.NotNil(t, actorRef, "registerWithSystem should return a valid ActorRef")
require.Equal(t, actorID, actorRef.ID(), "registered actor ID mismatch")
// The actor should be found in the receptionist.
foundActors := FindInReceptionist(as.Receptionist(), serviceKey)
require.Len(t, foundActors, 1, "actor not found in receptionist")
require.Equal(t, actorRef, foundActors[0], "incorrect actor in receptionist")
// Next, we'll send out a simple tell, using our reply channel to make
// sure it's actually processed.
msgData := "hello-system-actor"
replyChan := make(chan string, 1)
actorRef.Tell(context.Background(), newTestMsgWithReply(msgData, replyChan))
received, err := fn.RecvOrTimeout(replyChan, 100*time.Millisecond)
require.NoError(t, err, "timed out waiting for actor to process message")
require.Equal(t, msgData, received, "actor did not process message")
// Stop the actor through the system.
stopped := as.StopAndRemoveActor(actorID)
require.True(t, stopped, "StopAndRemoveActor failed")
// Wait for actor to fully stop.
time.Sleep(50 * time.Millisecond)
// Send a message to the now-stopped actor's ref. This should go to the
// system's DLO.
afterStopMsg := newTestMsg("after-stop-to-dlo")
require.NotPanics(t, func() {
actorRef.Tell(context.Background(), afterStopMsg)
}, "tell to stopped actor should not panic")
}
// TestActorSystemShutdown verifies that all actors are stopped and the system
// context is cancelled upon shutdown.
func TestActorSystemShutdown(t *testing.T) {
t.Parallel()
as := NewActorSystem()
// We'll start by making 3 new actors, each with a unique ID.
numActors := 3
actorRefs := make([]ActorRef[*testMsg, string], numActors)
for i := 0; i < numActors; i++ {
actorID := fmt.Sprintf("shutdown-test-actor-%d", i)
key := NewServiceKey[*testMsg, string](
fmt.Sprintf("service-%d", i),
)
beh := newEchoBehavior(t, 0)
ref, regErr := RegisterWithSystem(as, actorID, key, beh)
require.NoError(t, regErr)
actorRefs[i] = ref
}
// We'll now send a message to each actor to ensure that they're
// running.
for i, ref := range actorRefs {
future := ref.Ask(
context.Background(),
newTestMsg(fmt.Sprintf("ping-%d", i)),
)
ctxAwait, cancelAwait := context.WithTimeout(
context.Background(), time.Second,
)
res := future.Await(ctxAwait)
cancelAwait()
require.False(
t, res.IsErr(),
"actor %d failed to respond before shutdown: %v",
i, res.Err(),
)
}
// Next, trigger a shutdown, and assert that the done channel gets
// closed.
err := as.Shutdown()
require.NoError(t, err, "actorSystem shutdown failed")
// Check if the system context is done using RecvOrTimeout with a zero
// timeout for a non-blocking check.
_, err = fn.RecvOrTimeout(as.ctx.Done(), time.Millisecond*100)
require.NoError(t, err, "actorSystem context not cancelled after shutdown")
// We'll now try to send a message to each of the actors, this should
// result in an error.
for i, ref := range actorRefs {
future := ref.Ask(
context.Background(),
newTestMsg(fmt.Sprintf("ping-after-shutdown-%d", i)),
)
res := future.Await(context.Background())
require.True(
t, res.IsErr(),
"actor %d Ask should fail after shutdown", i,
)
require.ErrorIs(t, res.Err(), ErrActorTerminated)
}
as.mu.RLock()
require.Nil(t, as.actors, "actors map should be nil after shutdown")
as.mu.RUnlock()
// Once shutdown, we shouldn't be able to send to the DLO either.
dloRef := as.DeadLetters()
futureDLO := dloRef.Ask(
context.Background(), newTestMsg("ping-dlo-after-shutdown"),
)
resDLO := futureDLO.Await(context.Background())
require.True(
t, resDLO.IsErr(), "DLO Ask should fail after system shutdown",
)
require.ErrorIs(
t, resDLO.Err(), ErrActorTerminated,
)
}
// TestActorSystemStopAndRemoveActor verifies specific actor stopping and
// removal.
func TestActorSystemStopAndRemoveActor(t *testing.T) {
t.Parallel()
as := NewActorSystem()
defer func() {
err := as.Shutdown()
require.NoError(t, err)
}()
// Make some actor IDs, then unique service keys, then use that to
// register two actors.
actor1ID := "actor-to-stop"
actor2ID := "actor-to-keep"
key1 := NewServiceKey[*testMsg, string]("service1")
key2 := NewServiceKey[*testMsg, string]("service2")
beh := newEchoBehavior(t, 0)
ref1, err := RegisterWithSystem(as, actor1ID, key1, beh)
require.NoError(t, err)
ref2, err := RegisterWithSystem(as, actor2ID, key2, beh)
require.NoError(t, err)
// If we remove one actor, then try to send to it, we should get an
// error.
stopped := as.StopAndRemoveActor(actor1ID)
require.True(t, stopped, "failed to stop and remove actor1")
future1 := ref1.Ask(context.Background(), newTestMsg("ping-actor1"))
res1 := future1.Await(context.Background())
require.True(t, res1.IsErr(), "actor1 should be stopped")
require.ErrorIs(t, res1.Err(), ErrActorTerminated)
as.mu.RLock()
_, exists := as.actors[actor1ID]
as.mu.RUnlock()
// The actor should no longer be found.
require.False(t, exists, "actor1 still in system's actor map")
// Make sure that we can still send messages to the existing actor.
future2 := ref2.Ask(
context.Background(), newTestMsg("ping-actor2"),
)
ctxAwait2, cancelAwait2 := context.WithTimeout(
context.Background(), time.Second,
)
res2 := future2.Await(ctxAwait2)
cancelAwait2()
require.False(
t, res2.IsErr(), "actor2 should still be running: %v",
res2.Err(),
)
res2.WhenOk(func(s string) {
require.Equal(t, "echo: ping-actor2", s)
})
stoppedNonExistent := as.StopAndRemoveActor("non-existent-actor")
require.False(
t, stoppedNonExistent, "stopping non-existent actor should "+
"return false",
)
}
// TestReceptionist covers basic registration, finding, and unregistration.
func TestReceptionist(t *testing.T) {
t.Parallel()
as := NewActorSystem()
defer func() {
err := as.Shutdown()
require.NoError(t, err)
}()
receptionist := as.Receptionist()
key1 := NewServiceKey[*testMsg, string]("key1")
key2 := NewServiceKey[*testMsg, string]("key2")
key1Again := NewServiceKey[*testMsg, string]("key1")
// Register 3 actor instance using the service keys we created above.
beh := newEchoBehavior(t, 0)
actor1Ref, err := RegisterWithSystem(as, "actor1-rec", key1, beh)
require.NoError(t, err)
actor2Ref, err := RegisterWithSystem(as, "actor2-rec", key1, beh)
require.NoError(t, err)
actor3Ref, err := RegisterWithSystem(as, "actor3-rec", key2, beh)
require.NoError(t, err)
// We should be able to find the actors we registered.
foundForKey1 := FindInReceptionist(receptionist, key1)
require.Len(t, foundForKey1, 2, "should find 2 actors for key1")
require.Contains(t, foundForKey1, actor1Ref)
require.Contains(t, foundForKey1, actor2Ref)
foundForKey1Again := FindInReceptionist(receptionist, key1Again)
require.ElementsMatch(t, foundForKey1, foundForKey1Again)
// Same goes for the second key we added.
foundForKey2 := FindInReceptionist(receptionist, key2)
require.Len(t, foundForKey2, 1, "should find 1 actor for key2")
require.Equal(t, actor3Ref, foundForKey2[0])
// We shouldn't be able to find a key we didn't add.
nonExistentKey := NewServiceKey[*testMsg, string]("non-existent")
foundForNonExistent := FindInReceptionist(receptionist, nonExistentKey)
require.Empty(t, foundForNonExistent)
// We should be able to unregister the actors we added.
unregistered := UnregisterFromReceptionist(
receptionist, key1, actor1Ref,
)
require.True(t, unregistered, "failed to unregister actor1Ref")
foundForKey1AfterUnreg := FindInReceptionist(receptionist, key1)
require.Len(t, foundForKey1AfterUnreg, 1)
require.Equal(t, actor2Ref, foundForKey1AfterUnreg[0])
// If we try to unregister the same actor again, it should fail.
unregisteredAgain := UnregisterFromReceptionist(receptionist, key1, actor1Ref)
require.False(t, unregisteredAgain)
unregisteredLast := UnregisterFromReceptionist(receptionist, key1, actor2Ref)
require.True(t, unregisteredLast)
foundForKey1AfterAllUnreg := FindInReceptionist(receptionist, key1)
require.Empty(t, foundForKey1AfterAllUnreg)
receptionist.mu.RLock()
_, exists := receptionist.registrations[key1.name]
receptionist.mu.RUnlock()
require.False(t, exists, "key1 should be removed from registrations map")
// Finally, if we use the wrong key, or one that doesn't exist, that
// should also fail.
unregisteredWrongKey := UnregisterFromReceptionist(receptionist, key1, actor3Ref)
require.False(t, unregisteredWrongKey)
unregisteredNonExistentKey := UnregisterFromReceptionist(receptionist, nonExistentKey, actor1Ref)
require.False(t, unregisteredNonExistentKey)
}
// TestServiceKeyMethods tests Spawn and Unregister methods on ServiceKey.
func TestServiceKeyMethods(t *testing.T) {
t.Parallel()
as := NewActorSystem()
defer func() {
err := as.Shutdown()
require.NoError(t, err)
}()
key := NewServiceKey[*testMsg, string]("sk-service")
beh := newEchoBehavior(t, 0)
// Attempt to spawn a new actor using the service key and desired
// behavior.
actorRef, err := key.Spawn(as, "actor-sk-spawn", beh)
require.NoError(t, err)
require.NotNil(t, actorRef)
require.Equal(t, "actor-sk-spawn", actorRef.ID())
// We should be able to find the actor in the receptionist.
found := FindInReceptionist(as.Receptionist(), key)
require.Len(t, found, 1)
require.Equal(t, actorRef, found[0])
as.mu.RLock()
_, sysExists := as.actors[actorRef.ID()]
as.mu.RUnlock()
require.True(t, sysExists)
// Next, try to unregister the actor using the service key.
success := key.Unregister(as, actorRef)
require.True(t, success, "serviceKey.Unregister failed")
// The actor should no longer be found in the receptionist.
foundAfter := FindInReceptionist(as.Receptionist(), key)
require.Empty(t, foundAfter)
as.mu.RLock()
_, sysExistsAfter := as.actors[actorRef.ID()]
as.mu.RUnlock()
require.False(t, sysExistsAfter)
// If we try to send a message to the actor after unregistering it, then
// we should get an error.
future := actorRef.Ask(context.Background(), newTestMsg("ping"))
res := future.Await(context.Background())
require.True(t, res.IsErr() && errors.Is(res.Err(), ErrActorTerminated))
successAgain := key.Unregister(as, actorRef)
require.False(t, successAgain)
otherSys := NewActorSystem() // Create a different actor system
defer func() {
err := otherSys.Shutdown()
require.NoError(t, err)
}()
// Create a dummy actor in otherSys of the correct generic type for the
// key. This actor won't be found in 'as', so Unregister should fail.
dummyBehOther := newEchoBehavior(t, 0)
dummyKeyOther := NewServiceKey[*testMsg, string]("dummy-other")
dummyActorRefOtherSys, err := RegisterWithSystem(
otherSys, "dummy-other-actor", dummyKeyOther, dummyBehOther,
)
require.NoError(t, err)
successNonMember := key.Unregister(as, dummyActorRefOtherSys)
require.False(t, successNonMember)
}
// TestServiceKeyUnregisterAll tests the UnregisterAll method on ServiceKey.
// It covers scenarios including basic unregistration of multiple actors,
// attempting to unregister with no actors present, unregistering actors for
// one key while leaving others intact, and the idempotency of the operation.
func TestServiceKeyUnregisterAll(t *testing.T) {
t.Parallel()
// Common setup for all sub-tests.
as := NewActorSystem()
defer func() {
err := as.Shutdown()
require.NoError(t, err, "ActorSystem shutdown failed.")
}()
// Common behavior for test actors used across sub-tests.
beh := newEchoBehavior(t, 0)
t.Run("unregister all multiple actors", func(st *testing.T) {
key1 := NewServiceKey[*testMsg, string]("sk-ua-key1")
actor1Key1, err := key1.Spawn(as, "actor1-k1-ua", beh)
require.NoError(st, err)
actor2Key1, err := key1.Spawn(as, "actor2-k1-ua", beh)
require.NoError(st, err)
// Verify they are registered in the receptionist.
foundActorsForKey1 := FindInReceptionist(
as.Receptionist(), key1,
)
require.Len(
st, foundActorsForKey1, 2,
"actors for key1 not in receptionist initially.",
)
// Verify they are in the system's actor map.
as.mu.RLock()
_, actor1Key1Exists := as.actors[actor1Key1.ID()]
_, actor2Key1Exists := as.actors[actor2Key1.ID()]
as.mu.RUnlock()
require.True(
st, actor1Key1Exists,
"actor1 for key1 not in system actors map initially.",
)
require.True(
st, actor2Key1Exists,
"actor2 for key1 not in system actors map initially.",
)
// Unregister all for key1.
stoppedCountKey1 := key1.UnregisterAll(as)
require.Equal(
st, 2, stoppedCountKey1,
"UnregisterAll for key1 returned incorrect count.",
)
// Verify they are unregistered from the receptionist.
foundActorsForKey1After := FindInReceptionist(
as.Receptionist(), key1,
)
require.Empty(
st, foundActorsForKey1After,
"actors for key1 still in receptionist after "+
"UnregisterAll.",
)
// Verify they are removed from system actors map.
as.mu.RLock()
_, actor1Key1ExistsAfter := as.actors[actor1Key1.ID()]
_, actor2Key1ExistsAfter := as.actors[actor2Key1.ID()]
as.mu.RUnlock()
require.False(
st, actor1Key1ExistsAfter,
"Actor1 for key1 still in system actors "+
"map after UnregisterAll.",
)
require.False(
st, actor2Key1ExistsAfter,
"Actor2 for key1 still in system actors "+
"map after UnregisterAll.",
)
// Verify actors are stopped.
resultActor1Key1 := actor1Key1.Ask(
context.Background(), newTestMsg("ping-k1-a1"),
).Await(context.Background())
require.True(
st, resultActor1Key1.IsErr(),
"Actor1 key1 Ask should fail after UnregisterAll.",
)
require.ErrorIs(
st, resultActor1Key1.Err(), ErrActorTerminated,
"Actor1 key1 not terminated with correct error.",
)
resultActor2Key1 := actor2Key1.Ask(
context.Background(), newTestMsg("ping-k1-a2"),
).Await(context.Background())
require.True(
st, resultActor2Key1.IsErr(),
"Actor2 key1 Ask should fail after UnregisterAll.",
)
require.ErrorIs(
st, resultActor2Key1.Err(), ErrActorTerminated,
"Actor2 key1 not terminated with correct error.",
)
})
t.Run("unregister all with no actors for the key", func(st *testing.T) {
keyEmpty := NewServiceKey[*testMsg, string]("sk-ua-key-empty")
stoppedCountEmptyKey := keyEmpty.UnregisterAll(as)
require.Equal(
st, 0, stoppedCountEmptyKey,
"UnregisterAll for empty key returned non-zero count.",
)
foundActorsForKeyEmpty := FindInReceptionist(
as.Receptionist(), keyEmpty,
)
require.Empty(
st, foundActorsForKeyEmpty,
"Receptionist not empty for keyEmpty "+
"after UnregisterAll.",
)
})
t.Run("unregister all with mixed keys", func(st *testing.T) {
keyA := NewServiceKey[*testMsg, string]("sk-ua-keyA")
keyB := NewServiceKey[*testMsg, string]("sk-ua-keyB")
// Spawn 3 actors, two of them will share the same service key.
actorA1, err := keyA.Spawn(as, "actorA1-ua-mixed", beh)
require.NoError(st, err)
actorA2, err := keyA.Spawn(as, "actorA2-ua-mixed", beh)
require.NoError(st, err)
actorB1, err := keyB.Spawn(as, "actorB1-ua-mixed", beh)
require.NoError(st, err)
// Make sure we're able to find them in the receptionist.
require.Len(
st, FindInReceptionist(as.Receptionist(), keyA), 2,
"KeyA initial registration count mismatch.",
)
require.Len(
st, FindInReceptionist(as.Receptionist(), keyB), 1,
"KeyB initial registration count mismatch.",
)
// We'll start by unregistering all actors for keyA.
stoppedCountKeyA := keyA.UnregisterAll(as)
require.Equal(
st, 2, stoppedCountKeyA,
"UnregisterAll for keyA returned incorrect count.",
)
// Verify keyA actors are gone from receptionist, keyB actor
// remains.
require.Empty(
st, FindInReceptionist(as.Receptionist(), keyA),
"actors for keyA still in receptionist after "+
"UnregisterAll.",
)
foundActorsForKeyBAfterA := FindInReceptionist(
as.Receptionist(), keyB,
)
require.Len(
st, foundActorsForKeyBAfterA, 1,
"Actor for keyB affected by UnregisterAll on keyA.",
)
require.Equal(
st, actorB1, foundActorsForKeyBAfterA[0],
"Wrong actor found for keyB.",
)
// Verify keyA actors are removed from system map, keyB actor
// remains.
as.mu.RLock()
_, actorA1ExistsAfterMixed := as.actors[actorA1.ID()]
_, actorA2ExistsAfterMixed := as.actors[actorA2.ID()]
_, actorB1ExistsAfterMixed := as.actors[actorB1.ID()]
as.mu.RUnlock()
require.False(
st, actorA1ExistsAfterMixed,
"ActorA1 still in system actors map after "+
"mixed UnregisterAll.",
)
require.False(
st, actorA2ExistsAfterMixed,
"ActorA2 still in system actors map after "+
"mixed UnregisterAll.",
)
require.True(
st, actorB1ExistsAfterMixed,
"ActorB1 removed from system actors map incorrectly.",
)
// Verify keyA actors are stopped, keyB actor is running.
resultActorA1Mixed := actorA1.Ask(
context.Background(), newTestMsg("ping-kA-a1"),
).Await(context.Background())
require.True(st, resultActorA1Mixed.IsErr())
require.ErrorIs(
st, resultActorA1Mixed.Err(), ErrActorTerminated,
)
resultActorB1Mixed := actorB1.Ask(
context.Background(), newTestMsg("ping-kB-a1"),
).Await(context.Background())
require.False(
st, resultActorB1Mixed.IsErr(),
"ActorB1 terminated incorrectly (mixed test): %v",
resultActorB1Mixed.Err(),
)
resultActorB1Mixed.WhenOk(func(s string) {
require.Equal(st, "echo: ping-kB-a1", s)
})
})
t.Run("idempotency of UnregisterAll", func(st *testing.T) {
keyIdempotent := NewServiceKey[*testMsg, string](
"sk-ua-key-idem",
)
actorIdem, err := keyIdempotent.Spawn(as, "actor-idem-ua", beh)
require.NoError(st, err)
// First call should unregister and stop.
stoppedCountFirstCall := keyIdempotent.UnregisterAll(as)
require.Equal(
st, 1, stoppedCountFirstCall,
"UnregisterAll (first call) incorrect count.",
)
// Second call should do nothing and return 0.
stoppedCountSecondCall := keyIdempotent.UnregisterAll(as)
require.Equal(
st, 0, stoppedCountSecondCall,
"UnregisterAll (second call) incorrect count, not "+
"idempotent.",
)
// Verify actor is gone from receptionist and system map, and is
// stopped.
require.Empty(
st, FindInReceptionist(as.Receptionist(), keyIdempotent),
"Actors for keyIdempotent still in receptionist "+
"after calls.",
)
as.mu.RLock()
_, actorIdemExistsAfter := as.actors[actorIdem.ID()]
as.mu.RUnlock()
require.False(
st, actorIdemExistsAfter,
"ActorIdem still in system actors map after calls.",
)
resultActorIdem := actorIdem.Ask(
context.Background(), newTestMsg("ping-kidem-a1"),
).Await(context.Background())
require.True(st, resultActorIdem.IsErr())
require.ErrorIs(st, resultActorIdem.Err(), ErrActorTerminated)
})
}
// routerTestHarness helps set up routers and their associated actors for testing.
// It uses an actorTestHarness internally for DLO observation for the router.
type routerTestHarness struct {
*actorTestHarness
as *ActorSystem
receptionist *Receptionist
}
// newRouterTestHarness sets up a new harness for router testing.
// It creates an ActorSystem for actors that the router will route to,
// and uses the embedded actorTestHarness for the router's own DLO.
func newRouterTestHarness(t *testing.T) *routerTestHarness {
t.Helper()
system := NewActorSystem()
t.Cleanup(func() {
err := system.Shutdown()
require.NoError(t, err, "router test actor system shutdown failed")
})
// The DLO for the router itself will come from actorTestHarness.
// Actors managed by `system` (router targets) will use `system.DeadLetters()`.
return &routerTestHarness{
actorTestHarness: newActorTestHarness(t),
as: system,
receptionist: system.Receptionist(),
}
}
// newRouterTargetActor creates an actor, registers it with the harness's
// ActorSystem (h.as) and Receptionist under the given service key. This actor
// is intended to be a target for the router.
func (h *routerTestHarness) newRouterTargetActor(id string,
key ServiceKey[*testMsg, string],
beh ActorBehavior[*testMsg, string]) ActorRef[*testMsg, string] {
h.t.Helper()
ref, err := RegisterWithSystem(h.as, id, key, beh)
require.NoError(h.t, err)
return ref
}
// TestRouterNewRouter verifies that a new router can be created as expected.
func TestRouterNewRouter(t *testing.T) {
t.Parallel()
h := newRouterTestHarness(t)
key := NewServiceKey[*testMsg, string]("router-service")
strategy := NewRoundRobinStrategy[*testMsg, string]()
router := NewRouter(h.receptionist, key, strategy, h.dlo.Ref())
require.NotNil(t, router, "newRouter should not return nil")
require.Equal(t, "router(router-service)", router.ID(), "router ID mismatch")
}
// countingEchoBehavior is an echo behavior that also counts how many messages
// it has processed.
type countingEchoBehavior struct {
*echoBehavior
id string
processedMsgs int64
}
func newCountingEchoBehavior(t *testing.T, id string) *countingEchoBehavior {
return &countingEchoBehavior{
echoBehavior: newEchoBehavior(t, 0),
id: id,
}
}
func (b *countingEchoBehavior) Receive(ctx context.Context,
msg *testMsg) fn.Result[string] {
atomic.AddInt64(&b.processedMsgs, 1)
// Include actor ID in reply for easier verification.
res := b.echoBehavior.Receive(ctx, msg)
val, err := res.Unpack()
if err == nil {
return fn.Ok(fmt.Sprintf("%s:%s", b.id, val))
}
return res
}
// TestRouterTellAndAskRoundRobin verifies that the router distributes messages
// in a round robin properly.
func TestRouterTellAndAskRoundRobin(t *testing.T) {
t.Parallel()
h := newRouterTestHarness(t)
// Make a new router for the given service key and round robin strategy.
serviceKey := NewServiceKey[*testMsg, string]("rr-service")
strategy := NewRoundRobinStrategy[*testMsg, string]()
router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref())
// We'll now register two actors with the router, each with a different
// service key.
actor1Beh := newCountingEchoBehavior(t, "actor1")
actor2Beh := newCountingEchoBehavior(t, "actor2")
_ = h.newRouterTargetActor("actor1-rr", serviceKey, actor1Beh)
_ = h.newRouterTargetActor("actor2-rr", serviceKey, actor2Beh)
// Nxet, we'll send a mix of Tell and Ask messages to the router.
numMessages := 6
for i := 0; i < numMessages; i++ {
msgData := fmt.Sprintf("message-%d", i)
if i%2 == 0 {
router.Tell(context.Background(), newTestMsg(msgData))
} else {
future := router.Ask(
context.Background(), newTestMsg(msgData),
)
ctxAwait, cancelAwait := context.WithTimeout(
context.Background(), time.Second,
)
result := future.Await(ctxAwait)
cancelAwait()
require.False(
t, result.IsErr(), "ask failed: %v", result.Err(),
)
}
}
// Wait a bit for Tell messages to be processed.
time.Sleep(100 * time.Millisecond)
// Each actor should have processed numMessages / 2 messages.
require.EqualValues(
t, numMessages/2, atomic.LoadInt64(&actor1Beh.processedMsgs),
"actor1 processed message count mismatch",
)
require.EqualValues(
t, numMessages/2, atomic.LoadInt64(&actor2Beh.processedMsgs),
"actor2 processed message count mismatch",
)
// Router's DLO should be empty.
h.assertNoDLOMessages()
}
// TestRouterNoActorsAvailable verifies that if no actors are available for the
// message, then an error is returned.
func TestRouterNoActorsAvailable(t *testing.T) {
t.Parallel()
h := newRouterTestHarness(t)
serviceKey := NewServiceKey[*testMsg, string]("no-actor-service")
strategy := NewRoundRobinStrategy[*testMsg, string]()
router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref())
// We'll send a message, then assert that it goes to the DLO.
tellMsg := newTestMsg("tell-no-actor")
router.Tell(context.Background(), tellMsg)
h.assertDLOMessage(tellMsg)
// If we use an ask instead, then we should get an error.
askMsg := newTestMsg("ask-no-actor")
future := router.Ask(context.Background(), askMsg)
result := future.Await(context.Background())
require.True(
t, result.IsErr(), "ask should fail when no actors are available",
)
require.ErrorIs(t, result.Err(), ErrNoActorsAvailable, "error mismatch")
}
// TestRouterTellAskContextCancellation verifies that if the context is
// canceled, then sending aborts.
func TestRouterTellAskContextCancellation(t *testing.T) {
t.Parallel()
h := newRouterTestHarness(t)
serviceKey := NewServiceKey[*testMsg, string]("ctx-cancel-service")
strategy := NewRoundRobinStrategy[*testMsg, string]()
router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref())
// Use a regular echo actor, but we'll control context for Tell/Ask.
targetActorBeh := newEchoBehavior(t, 50*time.Millisecond)
_ = h.newRouterTargetActor("target-ctx", serviceKey, targetActorBeh)
// Next, we'll send a Tell message with a context that will be cancelled
// before we even send.
ctxTell, cancelTell := context.WithCancel(context.Background())
cancelTell()
router.Tell(ctxTell, newTestMsg("tell-ctx-cancelled"))
// The Message should be dropped by actorRefImpl.Tell if ctx is
// cancelled. Router's DLO should not receive it from this path.
h.assertNoDLOMessages()
// Next, we'll do the same for Ask. This time, we should get an error.
ctxAsk, cancelAsk := context.WithCancel(context.Background())
cancelAsk()
futureAsk := router.Ask(ctxAsk, newTestMsg("ask-ctx-cancelled"))
resultAsk := futureAsk.Await(context.Background())
require.True(
t, resultAsk.IsErr(), "ask with cancelled context should fail",
)
require.ErrorIs(
t, resultAsk.Err(), context.Canceled,
"error should be context.Canceled",
)
}
// TestRouterDynamicActorRegistration tests that we're able to dynamically add
// and remove actors from the router.
func TestRouterDynamicActorRegistration(t *testing.T) {
t.Parallel()
h := newRouterTestHarness(t)
serviceKey := NewServiceKey[*testMsg, string]("dynamic-service")
strategy := NewRoundRobinStrategy[*testMsg, string]()
router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref())
// If we try to send a mesasge to the router before any actors are
// added, we should get an error.
futureNoActor := router.Ask(context.Background(), newTestMsg("ping-no-actors"))
resNoActor := futureNoActor.Await(context.Background())
require.ErrorIs(t, resNoActor.Err(), ErrNoActorsAvailable)
actor1Beh := newCountingEchoBehavior(t, "actor1")
actor1Ref := h.newRouterTargetActor("actor1-dynamic", serviceKey, actor1Beh)
// At this point, we have a new actor added, but we'll try to send a
// message to a different actor ID. This should go to the router's DLO.
futureActor1 := router.Ask(context.Background(), newTestMsg("ping-actor1"))
ctxAwaitA1, cancelAwaitA1 := context.WithTimeout(context.Background(), time.Second)
resActor1 := futureActor1.Await(ctxAwaitA1)
cancelAwaitA1()
require.False(t, resActor1.IsErr(), "ask to actor1 failed: %v", resActor1.Err())
resActor1.WhenOk(func(s string) {
require.Equal(t, "actor1:echo: ping-actor1", s)
})
actor2Beh := newCountingEchoBehavior(t, "actor2")
actor2Ref := h.newRouterTargetActor(
"actor2-dynamic", serviceKey, actor2Beh,
)
// Now that we've added two actors above, we should round robin between
// them when sending.
ctxAwaitDA1, cancelAwaitDA1 := context.WithTimeout(
context.Background(), time.Second,
)
router.Ask(context.Background(), newTestMsg("dynamic-ask1")).Await(
ctxAwaitDA1,
)
cancelAwaitDA1()
ctxAwaitDA2, cancelAwaitDA2 := context.WithTimeout(context.Background(), time.Second)
router.Ask(context.Background(), newTestMsg("dynamic-ask2")).Await(ctxAwaitDA2)
cancelAwaitDA2()
time.Sleep(50 * time.Millisecond)
// actor1 should have processed 2 messages (ping-actor1, dynamic-ask1),
require.EqualValues(t, 2, atomic.LoadInt64(&actor1Beh.processedMsgs))
require.EqualValues(t, 1, atomic.LoadInt64(&actor2Beh.processedMsgs))
// Next, we'll unregister the first actor ref.
unregistered := UnregisterFromReceptionist(
h.receptionist, serviceKey, actor1Ref,
)
require.True(t, unregistered)
// All the messages should now go to the second actor.
for i := 0; i < 2; i++ {
msgData := fmt.Sprintf("to-actor2-%d", i)
future := router.Ask(context.Background(), newTestMsg(msgData))
ctxAwaitLoop, cancelAwaitLoop := context.WithTimeout(
context.Background(), time.Second,
)
res := future.Await(ctxAwaitLoop)
cancelAwaitLoop()
require.False(
t, res.IsErr(), "ask to actor2 failed: %v", res.Err(),
)
res.WhenOk(func(s string) {
require.Equal(t, "actor2:echo: "+msgData, s)
})
}
// Actor 1 shouldn't have got any of the messages, they should go to
// actor 2.
require.EqualValues(t, 2, atomic.LoadInt64(&actor1Beh.processedMsgs))
require.EqualValues(t, 1+2, atomic.LoadInt64(&actor2Beh.processedMsgs))
// Next, we'll unregister the second actor ref.
unregistered2 := UnregisterFromReceptionist(
h.receptionist, serviceKey, actor2Ref,
)
require.True(t, unregistered2)
// If we try to send another message, it should go to the DL.
tellMsg := newTestMsg("dynamic-tell-no-actors")
router.Tell(context.Background(), tellMsg)
h.assertDLOMessage(tellMsg)
}

View file

@ -259,7 +259,6 @@ func TestGetNextScid(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
nextScid := getNextScid(test.current)
require.Equal(t, test.expected, nextScid)

View file

@ -43,7 +43,6 @@ var sharerTests = []sharerTest{
// receiver, produce identical child hashes and preimages as the sender.
func TestSharer(t *testing.T) {
for _, test := range sharerTests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

View file

@ -10,7 +10,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire"

View file

@ -1,7 +1,7 @@
package autopilot
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
)
// AgentConstraints is an interface the agent will query to determine what

View file

@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/lightningnetwork/lnd/lnwire"
)

View file

@ -10,8 +10,8 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/stretchr/testify/require"
)

View file

@ -40,7 +40,6 @@ func TestBetweennessCentralityEmptyGraph(t *testing.T) {
)
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -83,7 +82,6 @@ func TestBetweennessCentralityWithNonEmptyGraph(t *testing.T) {
for _, numWorkers := range workers {
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
numWorkers := numWorkers
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -110,7 +108,6 @@ func TestBetweennessCentralityWithNonEmptyGraph(t *testing.T) {
require.NoError(t1, err)
for _, expected := range tests {
expected := expected
centrality := metric.GetMetric(
expected.normalize,
)

View file

@ -4,7 +4,7 @@ import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/stretchr/testify/require"
)

View file

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
)
// WeightedHeuristic is a tuple that associates a weight to an

View file

@ -5,7 +5,7 @@ import (
"fmt"
"sync"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
)
// ExternalScoreAttachment is an implementation of the AttachmentHeuristic

View file

@ -4,7 +4,7 @@ import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/lightningnetwork/lnd/autopilot"
)

View file

@ -8,7 +8,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
@ -101,25 +101,23 @@ func (d *databaseChannelGraph) ForEachNode(ctx context.Context,
}, reset)
}
// ForEachNodesChannels iterates through all connected nodes, and for each node,
// all the channels that connect to it. The passed callback will be called with
// the context, the Node itself, and a slice of ChannelEdge that connect to the
// node.
// ForEachNodesChannels iterates through all connected nodes, and for each
// node, all the channels that connect to it. The passed callback will be
// called with the context, the node's pubkey, and a slice of ChannelEdge
// that connect to the node.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (d *databaseChannelGraph) ForEachNodesChannels(ctx context.Context,
cb func(context.Context, Node, []*ChannelEdge) error,
cb func(context.Context, NodeID, []*ChannelEdge) error,
reset func()) error {
// The channel-scoring callers only need topology data here. Address
// filtering happens through ForEachNode before connecting to peers.
return d.db.ForEachNodeCached(
ctx, true, func(ctx context.Context, node route.Vertex,
addrs []net.Addr,
ctx, func(ctx context.Context, node route.Vertex,
chans map[uint64]*graphdb.DirectedChannel) error {
// We'll skip over any node that doesn't have any
// advertised addresses. As we won't be able to reach
// them to actually open any channels.
if len(addrs) == 0 {
if len(chans) == 0 {
return nil
}
@ -134,10 +132,7 @@ func (d *databaseChannelGraph) ForEachNodesChannels(ctx context.Context,
})
}
return cb(ctx, &dbNode{
pub: node,
addrs: addrs,
}, edges)
return cb(ctx, NodeID(node), edges)
}, reset,
)
}
@ -196,8 +191,8 @@ func (nc dbNodeCached) Addrs() []net.Addr {
func (dc *databaseChannelGraphCached) ForEachNode(ctx context.Context,
cb func(context.Context, Node) error, reset func()) error {
return dc.db.ForEachNodeCached(ctx, false, func(ctx context.Context,
n route.Vertex, _ []net.Addr,
return dc.db.ForEachNodeCached(ctx, func(ctx context.Context,
n route.Vertex,
channels map[uint64]*graphdb.DirectedChannel) error {
if len(channels) > 0 {
@ -213,20 +208,24 @@ func (dc *databaseChannelGraphCached) ForEachNode(ctx context.Context,
}, reset)
}
// ForEachNodesChannels iterates through all connected nodes, and for each node,
// all the channels that connect to it. The passed callback will be called with
// the context, the Node itself, and a slice of ChannelEdge that connect to the
// node.
// ForEachNodesChannels iterates through all connected nodes, and for each
// node, all the channels that connect to it. The passed callback will be
// called with the context, the node's pubkey, and a slice of ChannelEdge
// that connect to the node.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (dc *databaseChannelGraphCached) ForEachNodesChannels(ctx context.Context,
cb func(context.Context, Node, []*ChannelEdge) error,
cb func(context.Context, NodeID, []*ChannelEdge) error,
reset func()) error {
return dc.db.ForEachNodeCached(ctx, false, func(ctx context.Context,
n route.Vertex, _ []net.Addr,
return dc.db.ForEachNodeCached(ctx, func(ctx context.Context,
n route.Vertex,
channels map[uint64]*graphdb.DirectedChannel) error {
if len(channels) == 0 {
return nil
}
edges := make([]*ChannelEdge, 0, len(channels))
for cid, channel := range channels {
edges = append(edges, &ChannelEdge{
@ -236,18 +235,7 @@ func (dc *databaseChannelGraphCached) ForEachNodesChannels(ctx context.Context,
})
}
if len(channels) > 0 {
node := dbNodeCached{
node: n,
channels: channels,
}
if err := cb(ctx, node, edges); err != nil {
return err
}
}
return nil
return cb(ctx, NodeID(n), edges)
}, reset)
}

View file

@ -3,7 +3,7 @@ package autopilot_test
import (
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/lightningnetwork/lnd/autopilot"
)

View file

@ -5,8 +5,8 @@ import (
"net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/wire/v2"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
@ -84,10 +84,10 @@ type ChannelGraph interface {
// ForEachNodesChannels iterates through all connected nodes, and for
// each node, all the channels that connect to it. The passed callback
// will be called with the context, the Node itself, and a slice of
// will be called with the context, the node's pubkey, and a slice of
// ChannelEdge that connect to the node.
ForEachNodesChannels(ctx context.Context,
cb func(context.Context, Node, []*ChannelEdge) error,
cb func(context.Context, NodeID, []*ChannelEdge) error,
reset func()) error
}
@ -237,9 +237,8 @@ type GraphSource interface {
// channel graph cache if one is available. It is less consistent than
// ForEachNode since any further calls are made across multiple
// transactions.
ForEachNodeCached(ctx context.Context, withAddrs bool,
ForEachNodeCached(ctx context.Context,
cb func(ctx context.Context, node route.Vertex,
addrs []net.Addr,
chans map[uint64]*graphdb.DirectedChannel) error,
reset func()) error
}

View file

@ -6,7 +6,7 @@ import (
"sync"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/wire/v2"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
)
// minMedianChanSizeFraction determines the minimum size a channel must have to
@ -90,7 +90,7 @@ func (p *PrefAttachment) NodeScores(ctx context.Context, g ChannelGraph,
seenChans = make(map[uint64]struct{})
)
err := g.ForEachNodesChannels(
ctx, func(_ context.Context, node Node,
ctx, func(_ context.Context, node NodeID,
channels []*ChannelEdge) error {
for _, e := range channels {
@ -121,7 +121,7 @@ func (p *PrefAttachment) NodeScores(ctx context.Context, g ChannelGraph,
var maxChans int
nodeChanNum := make(map[NodeID]int)
err = g.ForEachNodesChannels(
ctx, func(ctx context.Context, node Node,
ctx, func(ctx context.Context, node NodeID,
edges []*ChannelEdge) error {
var nodeChans int
@ -154,17 +154,16 @@ func (p *PrefAttachment) NodeScores(ctx context.Context, g ChannelGraph,
// If this node is not among our nodes to score, we can
// return early.
nID := NodeID(node.PubKey())
if _, ok := nodes[nID]; !ok {
if _, ok := nodes[node]; !ok {
log.Tracef("Node %x not among nodes to score, "+
"ignoring", nID[:])
"ignoring", node[:])
return nil
}
// Otherwise we'll record the number of channels.
nodeChanNum[nID] = nodeChans
nodeChanNum[node] = nodeChans
log.Tracef("Counted %v channels for node %x", nodeChans,
nID[:])
node[:])
return nil
}, func() {

View file

@ -11,7 +11,8 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
@ -31,12 +32,14 @@ type testGraph interface {
}
type testDBGraph struct {
db *graphdb.ChannelGraph
db *graphdb.VersionedGraph
databaseChannelGraph
}
func newDiskChanGraph(t *testing.T) (testGraph, error) {
graphDB := graphdb.MakeTestGraph(t)
graphDB := graphdb.NewVersionedGraph(
graphdb.MakeTestGraph(t), lnwire.GossipVersion1,
)
require.NoError(t, graphDB.Start())
t.Cleanup(func() {
require.NoError(t, graphDB.Stop())
@ -88,7 +91,6 @@ func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
}
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -125,7 +127,6 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
)
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -212,7 +213,6 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
)
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -243,11 +243,11 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
twoChans := false
nodes := make(map[NodeID]struct{})
err = graph.ForEachNodesChannels(
ctx, func(_ context.Context, node Node,
ctx, func(_ context.Context, node NodeID,
edges []*ChannelEdge) error {
numNodes++
nodes[node.PubKey()] = struct{}{}
nodes[node] = struct{}{}
numChans := 0
for range edges {
@ -325,7 +325,6 @@ func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
)
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
graph, err := chanGraph.genFunc(t)
require.NoError(t, err, "unable to create graph")
@ -417,19 +416,21 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
case errors.Is(err, graphdb.ErrGraphNodeNotFound):
fallthrough
case errors.Is(err, graphdb.ErrGraphNotFound):
graphNode := &models.Node{
HaveNodeAnnouncement: true,
Addresses: []net.Addr{&net.TCPAddr{
IP: bytes.Repeat(
[]byte("a"), 16,
),
}},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
),
AuthSigBytes: testSig.Serialize(),
}
graphNode.AddPubKey(pub)
//nolint:ll
graphNode := models.NewV1Node(
route.NewVertex(pub),
&models.NodeV1Fields{
Addresses: []net.Addr{&net.TCPAddr{
IP: bytes.Repeat(
[]byte("a"), 16,
),
}},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
).RawFeatureVector,
AuthSigBytes: testSig.Serialize(),
},
)
err := d.db.AddNode(
context.Background(), graphNode,
)
@ -447,19 +448,18 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
if err != nil {
return nil, err
}
dbNode := &models.Node{
HaveNodeAnnouncement: true,
Addresses: []net.Addr{
&net.TCPAddr{
dbNode := models.NewV1Node(
route.NewVertex(nodeKey), &models.NodeV1Fields{
Addresses: []net.Addr{&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
}},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
).RawFeatureVector,
AuthSigBytes: testSig.Serialize(),
},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
),
AuthSigBytes: testSig.Serialize(),
}
dbNode.AddPubKey(nodeKey)
)
if err := d.db.AddNode(
context.Background(), dbNode,
); err != nil {
@ -489,16 +489,26 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
}
chanID := randChanID()
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
Capacity: capacity,
Features: lnwire.EmptyFeatureVector(),
nodeKey1 := route.NewVertex(lnNode1)
nodeKey2 := route.NewVertex(lnNode2)
btcKey1 := route.NewVertex(lnNode1)
btcKey2 := route.NewVertex(lnNode2)
edge, err := models.NewV1Channel(
chanID.ToUint64(), chainhash.Hash{}, nodeKey1, nodeKey2,
&models.ChannelV1Fields{
BitcoinKey1Bytes: btcKey1,
BitcoinKey2Bytes: btcKey2,
}, models.WithCapacity(capacity),
)
if err != nil {
return nil, nil, err
}
edge.AddNodeKeys(lnNode1, lnNode2, lnNode1, lnNode2)
if err := d.db.AddChannelEdge(ctx, edge); err != nil {
return nil, nil, err
}
edgePolicy := &models.ChannelEdgePolicy{
Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID.ToUint64(),
LastUpdate: time.Now(),
@ -515,6 +525,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
return nil, nil, err
}
edgePolicy = &models.ChannelEdgePolicy{
Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID.ToUint64(),
LastUpdate: time.Now(),
@ -548,19 +559,19 @@ func (d *testDBGraph) addRandNode() (*btcec.PublicKey, error) {
if err != nil {
return nil, err
}
dbNode := &models.Node{
HaveNodeAnnouncement: true,
Addresses: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
dbNode := models.NewV1Node(
route.NewVertex(nodeKey), &models.NodeV1Fields{
Addresses: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
).RawFeatureVector,
AuthSigBytes: testSig.Serialize(),
},
Features: lnwire.NewFeatureVector(
nil, lnwire.Features,
),
AuthSigBytes: testSig.Serialize(),
}
dbNode.AddPubKey(nodeKey)
)
err = d.db.AddNode(context.Background(), dbNode)
if err != nil {
return nil, err
@ -604,14 +615,15 @@ func (m *memChannelGraph) ForEachNode(ctx context.Context,
return nil
}
// ForEachNodesChannels iterates through all connected nodes, and for each node,
// all the channels that connect to it. The passed callback will be called with
// the context, the Node itself, and a slice of ChannelEdge that connect to the
// node.
// ForEachNodesChannels iterates through all connected nodes, and for each
// node, all the channels that connect to it. The passed callback will be
// called with the context, the node's pubkey, and a slice of ChannelEdge
// that connect to the node.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (m *memChannelGraph) ForEachNodesChannels(ctx context.Context,
cb func(context.Context, Node, []*ChannelEdge) error, _ func()) error {
cb func(context.Context, NodeID, []*ChannelEdge) error,
_ func()) error {
for _, node := range m.graph {
edges := make([]*ChannelEdge, 0, len(node.chans))
@ -619,7 +631,7 @@ func (m *memChannelGraph) ForEachNodesChannels(ctx context.Context,
edges = append(edges, &node.chans[i])
}
if err := cb(ctx, node, edges); err != nil {
if err := cb(ctx, NewNodeID(node.pub), edges); err != nil {
return err
}
}

View file

@ -2,8 +2,6 @@ package autopilot
import (
"context"
"github.com/lightningnetwork/lnd/routing/route"
)
// diameterCutoff is used to discard nodes in the diameter calculation.
@ -35,12 +33,11 @@ func NewSimpleGraph(ctx context.Context, g ChannelGraph) (*SimpleGraph, error) {
// The returned index is then used to create a simplified adjacency list
// where each node is identified by its index instead of its pubkey, and
// also to create a mapping from node index to node pubkey.
getNodeIndex := func(node route.Vertex) int {
key := NodeID(node)
nodeIndex, ok := nodes[key]
getNodeIndex := func(node NodeID) int {
nodeIndex, ok := nodes[node]
if !ok {
nodes[key] = nextIndex
nodes[node] = nextIndex
nodeIndex = nextIndex
nextIndex++
}
@ -51,12 +48,12 @@ func NewSimpleGraph(ctx context.Context, g ChannelGraph) (*SimpleGraph, error) {
// Iterate over each node and each channel and update the adj and the
// node index.
err := g.ForEachNodesChannels(ctx, func(_ context.Context,
node Node, channels []*ChannelEdge) error {
node NodeID, channels []*ChannelEdge) error {
u := getNodeIndex(node.PubKey())
u := getNodeIndex(node)
for _, edge := range channels {
v := getNodeIndex(edge.Peer)
v := getNodeIndex(NodeID(edge.Peer))
adj[u] = append(adj[u], v)
}

View file

@ -4,7 +4,7 @@ import (
"context"
"runtime"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
)
// TopCentrality is a simple greedy technique to create connections to nodes

View file

@ -4,7 +4,7 @@ import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/stretchr/testify/require"
)
@ -83,7 +83,6 @@ func TestTopCentrality(t *testing.T) {
}
for _, chanGraph := range chanGraphs {
chanGraph := chanGraph
success := t.Run(chanGraph.name, func(t1 *testing.T) {
t1.Parallel()

View file

@ -1,9 +1,9 @@
package blockcache
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/cache"
"github.com/lightninglabs/neutrino/cache/lru"

View file

@ -6,9 +6,9 @@ import (
"sync"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/cache"
"github.com/stretchr/testify/require"

29
bolt12/decode.go Normal file
View file

@ -0,0 +1,29 @@
package bolt12
import (
"bytes"
"fmt"
"github.com/lightningnetwork/lnd/tlv"
)
// decodeStream runs a single typed-stream pass over data and returns the
// canonical TypeMap. Records may be passed in any order; NewStream requires
// them sorted, so SortRecords runs first.
func decodeStream(data []byte, records ...tlv.Record) (tlv.TypeMap, error) {
tlv.SortRecords(records)
stream, err := tlv.NewStream(records...)
if err != nil {
return nil, fmt.Errorf("create stream: %w", err)
}
typeMap, err := stream.DecodeWithParsedTypesP2P(
bytes.NewReader(data),
)
if err != nil {
return nil, fmt.Errorf("decode stream: %w", err)
}
return typeMap, nil
}

19
bolt12/doc.go Normal file
View file

@ -0,0 +1,19 @@
// Package bolt12 implements encoding, decoding, and validation for BOLT 12
// Offers, Invoice Requests, and Invoices. It provides a pure codec library
// with no LND daemon dependencies.
//
// BOLT 12 messages use TLV streams encoded with a checksumless bech32 variant
// and signed with BIP-340 Schnorr signatures over a Merkle tree of TLV fields.
//
// Human-readable prefixes:
// - lno: Offer
// - lnr: Invoice Request
// - lni: Invoice
//
// # Codec Contract
//
// Encode validates before serialising and refuses to emit bytes that would fail
// the writer requirements, invalid bytes are unrepresentable on the wire.
// Low-level decoders stay permissive so diagnostic and fuzz harnesses can
// inspect malformed input.
package bolt12

24
bolt12/helpers_test.go Normal file
View file

@ -0,0 +1,24 @@
package bolt12
import (
"bytes"
"github.com/btcsuite/btcd/btcec/v2"
)
// bobKey returns the deterministic spec test key for Bob, whose 32-byte scalar
// is 0x42 repeated. Used across signature and round-trip tests so the same key
// is not reconstructed in every callsite.
func bobKey() (*btcec.PrivateKey, *btcec.PublicKey) {
priv, pub := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x42}, 32))
return priv, pub
}
// aliceKey returns the deterministic spec test key for Alice, whose 32-byte
// scalar is 0x41 repeated.
func aliceKey() (*btcec.PrivateKey, *btcec.PublicKey) {
priv, pub := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x41}, 32))
return priv, pub
}

440
bolt12/invoice.go Normal file
View file

@ -0,0 +1,440 @@
package bolt12
import (
"bytes"
"fmt"
"maps"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// Invoice represents a BOLT 12 invoice message. It mirrors all non-signature
// invoice_request fields (types 0-91) and adds invoice-specific fields (types
// 160-176) plus a Schnorr signature (type 240).
//
// An invoice in response to a request should be constructed from that request
// (e.g., using NewInvoiceFromRequest) to mirror its fields. The caller then
// populates the invoice-specific fields and signs it.
type Invoice struct {
// Fields in the 0-91 range are mirrored verbatim from the
// invoice_request (which carries the offer's fields); the byte-for-byte
// match is enforced by ValidateInvoiceAgainstRequest.
// InvreqMetadata is the payer metadata.
InvreqMetadata tlv.OptionalRecordT[tlv.TlvType0, tlv.Blob]
// OfferChains are the chains the offer is valid for.
OfferChains tlv.OptionalRecordT[tlv.TlvType2, ChainsRecord]
// OfferMetadata is the offer metadata.
OfferMetadata tlv.OptionalRecordT[tlv.TlvType4, tlv.Blob]
// OfferCurrency is the offer currency.
OfferCurrency tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob]
// OfferAmount is the offer amount.
OfferAmount tlv.OptionalRecordT[tlv.TlvType8, TUint64]
// OfferDescription is the offer description.
OfferDescription tlv.OptionalRecordT[tlv.TlvType10, tlv.Blob]
// OfferFeatures are the offer features.
OfferFeatures tlv.OptionalRecordT[
tlv.TlvType12, lnwire.RawFeatureVector,
]
// OfferAbsoluteExpiry is the offer's absolute expiry.
OfferAbsoluteExpiry tlv.OptionalRecordT[tlv.TlvType14, TUint64]
// OfferPaths are the offer's blinded paths.
OfferPaths tlv.OptionalRecordT[tlv.TlvType16, lnwire.BlindedPaths]
// OfferIssuer is the offer issuer name.
OfferIssuer tlv.OptionalRecordT[tlv.TlvType18, tlv.Blob]
// OfferQuantityMax is the offer's maximum quantity.
OfferQuantityMax tlv.OptionalRecordT[tlv.TlvType20, TUint64]
// OfferIssuerID is the offer issuer's public key.
OfferIssuerID tlv.OptionalRecordT[tlv.TlvType22, *btcec.PublicKey]
// InvreqChain is the requested chain.
InvreqChain tlv.OptionalRecordT[tlv.TlvType80, [32]byte]
// InvreqAmount is the amount the payer offered.
InvreqAmount tlv.OptionalRecordT[tlv.TlvType82, TUint64]
// InvreqFeatures are the payer's features.
InvreqFeatures tlv.OptionalRecordT[
tlv.TlvType84, lnwire.RawFeatureVector,
]
// InvreqQuantity is the requested quantity.
InvreqQuantity tlv.OptionalRecordT[tlv.TlvType86, TUint64]
// InvreqPayerID is the payer's signing public key.
InvreqPayerID tlv.OptionalRecordT[tlv.TlvType88, *btcec.PublicKey]
// InvreqPayerNote is an optional payer note.
InvreqPayerNote tlv.OptionalRecordT[tlv.TlvType89, tlv.Blob]
// InvreqPaths are the payer's blinded paths to send the invoice to.
InvreqPaths tlv.OptionalRecordT[tlv.TlvType90, lnwire.BlindedPaths]
// InvreqBip353Name is the payer's BIP 353 name.
InvreqBip353Name tlv.OptionalRecordT[tlv.TlvType91, tlv.Blob]
// Fields from type 160 on are invoice-specific.
// InvoicePaths are the blinded paths to the recipient node.
InvoicePaths tlv.OptionalRecordT[tlv.TlvType160, lnwire.BlindedPaths]
// InvoiceBlindedPay carries one blinded_payinfo per invoice_paths
// entry, in order.
InvoiceBlindedPay tlv.OptionalRecordT[tlv.TlvType162, BlindedPayInfos]
// InvoiceCreatedAt is the creation time in seconds since the Unix
// epoch.
InvoiceCreatedAt tlv.OptionalRecordT[tlv.TlvType164, TUint64]
// InvoiceRelativeExp is the expiry in seconds after creation. When
// absent the spec default of 7200 seconds applies.
InvoiceRelativeExp tlv.OptionalRecordT[tlv.TlvType166, TUint32]
// InvoicePaymentHash is the SHA256 hash of the payment preimage.
InvoicePaymentHash tlv.OptionalRecordT[tlv.TlvType168, [32]byte]
// InvoiceAmount is the minimum amount the payee will accept, in the
// minimal payable unit of invreq_chain.
InvoiceAmount tlv.OptionalRecordT[tlv.TlvType170, TUint64]
// InvoiceFallbacks are optional on-chain fallback addresses.
InvoiceFallbacks tlv.OptionalRecordT[
tlv.TlvType172, FallbackAddresses,
]
// InvoiceFeatures are the features of the invoice.
InvoiceFeatures tlv.OptionalRecordT[
tlv.TlvType174, lnwire.RawFeatureVector,
]
// InvoiceNodeID is the public key of the recipient node, used to verify
// the signature.
InvoiceNodeID tlv.OptionalRecordT[tlv.TlvType176, *btcec.PublicKey]
// Signature is a BIP-340 Schnorr signature covering all fields.
Signature tlv.OptionalRecordT[tlv.TlvType240, [64]byte]
// decodedTLVs is the canonical TypeMap produced by the typed-stream
// pass that decoded this invoice. See Offer.decodedTLVs for the design
// rationale.
decodedTLVs tlv.TypeMap
}
// AllRecords returns the canonical sorted record list for this invoice, merging
// the typed records with any extra signed-range fields that the decoder
// preserved.
//
// NOTE: this is part of the tlv.PureTLVMessage interface.
func (inv *Invoice) AllRecords() []tlv.Record {
return allRecordsFromTypeMap(
inv.allRecordProducers(), inv.decodedTLVs,
)
}
var _ lnwire.PureTLVMessage = (*Invoice)(nil)
const (
// maxWitnessVersion is the highest segwit witness version a usable
// fallback address may carry; the BOLT 12 reader ignores anything
// above it.
maxWitnessVersion = 16
// minWitnessProgramLen and maxWitnessProgramLen bound the witness
// program length, in bytes, of a usable fallback address.
minWitnessProgramLen = 2
maxWitnessProgramLen = 40
)
// UsableFallbackAddresses returns the invoice_fallbacks entries a payer may use
// after applying the BOLT 12 reader's MUST-ignore rules for the bitcoin chain.
func (inv *Invoice) UsableFallbackAddresses() []FallbackAddress {
// Unwrap the optional up front so the filtering loop stays flat; a nil
// Addrs slice ranges as empty.
fallbacks := inv.InvoiceFallbacks.ValOpt().UnwrapOr(FallbackAddresses{})
var addrs []FallbackAddress
for _, a := range fallbacks.Addrs {
// MUST ignore any fallback_address for which version is greater
// than 16.
if a.Version > maxWitnessVersion {
continue
}
// MUST ignore any fallback_address for which address is less
// than 2 or greater than 40 bytes.
if len(a.Address) < minWitnessProgramLen ||
len(a.Address) > maxWitnessProgramLen {
continue
}
// MUST ignore any fallback_address for which address does not
// meet known requirements for the given version. NOT enforced
// here: the per-version witness-program check needs on-chain
// address rules above this codec, so a caller dispatching
// on-chain MUST apply it.
addrs = append(addrs, a)
}
return addrs
}
// UsablePath pairs a blinded path with its payment parameters, as returned by
// UsablePaths after the BOLT 12 reader's feature filter has been applied.
type UsablePath struct {
// Path is the blinded path to the recipient.
Path lnwire.BlindedPath
// PayInfo is the blinded_payinfo for Path.
PayInfo BlindedPayInfo
}
// UsablePaths returns the invoice_paths entries a payer may use, each paired
// with its blinded_payinfo, after applying the BOLT 12 reader rule that a path
// MUST NOT be used when its payinfo.features has unknown required (even) bits
// set. knownBlindedFeatures names the feature bits the reader understands.
//
// The result is empty when invoice_paths or invoice_blindedpay is absent, or
// when the two lists differ in length; ValidateInvoiceRead rejects those cases
// separately, so a caller that validates first can treat an empty result as
// "no usable paths".
func (inv *Invoice) UsablePaths(
knownBlindedFeatures map[lnwire.FeatureBit]string) []UsablePath {
paths := inv.InvoicePaths.ValOpt().UnwrapOr(lnwire.BlindedPaths{})
bp := inv.InvoiceBlindedPay.ValOpt().UnwrapOr(BlindedPayInfos{})
// Entries pair by index; a length mismatch is rejected upstream by
// ValidateInvoiceRead, so guard here to stay in bounds.
if len(paths.Paths) != len(bp.Infos) {
return nil
}
var usable []UsablePath
for i := range bp.Infos {
// MUST NOT use the path if payinfo.features has any unknown
// even bits set.
fv := bp.Infos[i].Features
wrapped := lnwire.NewFeatureVector(&fv, knownBlindedFeatures)
if len(wrapped.UnknownRequiredFeatures()) > 0 {
continue
}
usable = append(usable, UsablePath{
Path: paths.Paths[i],
PayInfo: bp.Infos[i],
})
}
return usable
}
// allRecordProducers returns record producers for all set fields.
func (inv *Invoice) allRecordProducers() []tlv.RecordProducer {
var p []tlv.RecordProducer
// Invreq mirrored fields.
lnwire.AddOpt(&p, inv.InvreqMetadata)
lnwire.AddOpt(&p, inv.OfferChains)
lnwire.AddOpt(&p, inv.OfferMetadata)
lnwire.AddOpt(&p, inv.OfferCurrency)
lnwire.AddOpt(&p, inv.OfferAmount)
lnwire.AddOpt(&p, inv.OfferDescription)
lnwire.AddOpt(&p, inv.OfferFeatures)
lnwire.AddOpt(&p, inv.OfferAbsoluteExpiry)
lnwire.AddOpt(&p, inv.OfferPaths)
lnwire.AddOpt(&p, inv.OfferIssuer)
lnwire.AddOpt(&p, inv.OfferQuantityMax)
lnwire.AddOpt(&p, inv.OfferIssuerID)
lnwire.AddOpt(&p, inv.InvreqChain)
lnwire.AddOpt(&p, inv.InvreqAmount)
lnwire.AddOpt(&p, inv.InvreqFeatures)
lnwire.AddOpt(&p, inv.InvreqQuantity)
lnwire.AddOpt(&p, inv.InvreqPayerID)
lnwire.AddOpt(&p, inv.InvreqPayerNote)
lnwire.AddOpt(&p, inv.InvreqPaths)
lnwire.AddOpt(&p, inv.InvreqBip353Name)
// Invoice-specific fields.
lnwire.AddOpt(&p, inv.InvoicePaths)
lnwire.AddOpt(&p, inv.InvoiceBlindedPay)
lnwire.AddOpt(&p, inv.InvoiceCreatedAt)
lnwire.AddOpt(&p, inv.InvoiceRelativeExp)
lnwire.AddOpt(&p, inv.InvoicePaymentHash)
lnwire.AddOpt(&p, inv.InvoiceAmount)
lnwire.AddOpt(&p, inv.InvoiceFallbacks)
lnwire.AddOpt(&p, inv.InvoiceFeatures)
lnwire.AddOpt(&p, inv.InvoiceNodeID)
lnwire.AddOpt(&p, inv.Signature)
return p
}
// Encode validates the invoice per writer requirements and serialises it via
// the PureTLVMessage shape.
func (inv *Invoice) Encode() ([]byte, error) {
if err := ValidateInvoiceWrite(inv); err != nil {
return nil, fmt.Errorf("validate invoice: %w", err)
}
var buf bytes.Buffer
if err := lnwire.EncodePureTLVMessage(inv, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecodeInvoice deserializes an invoice from a TLV byte stream. Decoding is
// permissive: callers that need spec compliance must run ValidateInvoiceRead.
func DecodeInvoice(data []byte) (*Invoice, error) {
var inv Invoice
invreqMetadata := tlv.ZeroRecordT[tlv.TlvType0, tlv.Blob]()
chains := tlv.ZeroRecordT[tlv.TlvType2, ChainsRecord]()
offerMeta := tlv.ZeroRecordT[tlv.TlvType4, tlv.Blob]()
currency := tlv.ZeroRecordT[tlv.TlvType6, tlv.Blob]()
offerAmt := tlv.ZeroRecordT[tlv.TlvType8, TUint64]()
desc := tlv.ZeroRecordT[tlv.TlvType10, tlv.Blob]()
offerFeat := tlv.ZeroRecordT[tlv.TlvType12, lnwire.RawFeatureVector]()
expiry := tlv.ZeroRecordT[tlv.TlvType14, TUint64]()
offerPaths := tlv.ZeroRecordT[tlv.TlvType16, lnwire.BlindedPaths]()
issuer := tlv.ZeroRecordT[tlv.TlvType18, tlv.Blob]()
qtyMax := tlv.ZeroRecordT[tlv.TlvType20, TUint64]()
issuerID := tlv.ZeroRecordT[tlv.TlvType22, *btcec.PublicKey]()
invreqChain := tlv.ZeroRecordT[tlv.TlvType80, [32]byte]()
invreqAmt := tlv.ZeroRecordT[tlv.TlvType82, TUint64]()
invreqFeat := tlv.ZeroRecordT[tlv.TlvType84, lnwire.RawFeatureVector]()
invreqQty := tlv.ZeroRecordT[tlv.TlvType86, TUint64]()
payerID := tlv.ZeroRecordT[tlv.TlvType88, *btcec.PublicKey]()
payerNote := tlv.ZeroRecordT[tlv.TlvType89, tlv.Blob]()
invreqPaths := tlv.ZeroRecordT[tlv.TlvType90, lnwire.BlindedPaths]()
bip353 := tlv.ZeroRecordT[tlv.TlvType91, tlv.Blob]()
invPaths := tlv.ZeroRecordT[tlv.TlvType160, lnwire.BlindedPaths]()
blindedPay := tlv.ZeroRecordT[tlv.TlvType162, BlindedPayInfos]()
createdAt := tlv.ZeroRecordT[tlv.TlvType164, TUint64]()
relExp := tlv.ZeroRecordT[tlv.TlvType166, TUint32]()
payHash := tlv.ZeroRecordT[tlv.TlvType168, [32]byte]()
invAmt := tlv.ZeroRecordT[tlv.TlvType170, TUint64]()
fallbacks := tlv.ZeroRecordT[tlv.TlvType172, FallbackAddresses]()
invFeat := tlv.ZeroRecordT[tlv.TlvType174, lnwire.RawFeatureVector]()
nodeID := tlv.ZeroRecordT[tlv.TlvType176, *btcec.PublicKey]()
sig := tlv.ZeroRecordT[tlv.TlvType240, [64]byte]()
tm, err := decodeStream(
data,
invreqMetadata.Record(), chains.Record(), offerMeta.Record(),
currency.Record(), offerAmt.Record(), desc.Record(),
offerFeat.Record(), expiry.Record(), offerPaths.Record(),
issuer.Record(), qtyMax.Record(), issuerID.Record(),
invreqChain.Record(), invreqAmt.Record(), invreqFeat.Record(),
invreqQty.Record(), payerID.Record(), payerNote.Record(),
invreqPaths.Record(), bip353.Record(), invPaths.Record(),
blindedPay.Record(), createdAt.Record(), relExp.Record(),
payHash.Record(), invAmt.Record(), fallbacks.Record(),
invFeat.Record(), nodeID.Record(), sig.Record(),
)
if err != nil {
return nil, fmt.Errorf("decode invoice: %w", err)
}
lnwire.SetOptFromMap(tm, &inv.InvreqMetadata, invreqMetadata)
lnwire.SetOptFromMap(tm, &inv.OfferChains, chains)
lnwire.SetOptFromMap(tm, &inv.OfferMetadata, offerMeta)
lnwire.SetOptFromMap(tm, &inv.OfferCurrency, currency)
lnwire.SetOptFromMap(tm, &inv.OfferAmount, offerAmt)
lnwire.SetOptFromMap(tm, &inv.OfferDescription, desc)
lnwire.SetOptFromMap(tm, &inv.OfferFeatures, offerFeat)
lnwire.SetOptFromMap(tm, &inv.OfferAbsoluteExpiry, expiry)
lnwire.SetOptFromMap(tm, &inv.OfferPaths, offerPaths)
lnwire.SetOptFromMap(tm, &inv.OfferIssuer, issuer)
lnwire.SetOptFromMap(tm, &inv.OfferQuantityMax, qtyMax)
lnwire.SetOptFromMap(tm, &inv.OfferIssuerID, issuerID)
lnwire.SetOptFromMap(tm, &inv.InvreqChain, invreqChain)
lnwire.SetOptFromMap(tm, &inv.InvreqAmount, invreqAmt)
lnwire.SetOptFromMap(tm, &inv.InvreqFeatures, invreqFeat)
lnwire.SetOptFromMap(tm, &inv.InvreqQuantity, invreqQty)
lnwire.SetOptFromMap(tm, &inv.InvreqPayerID, payerID)
lnwire.SetOptFromMap(tm, &inv.InvreqPayerNote, payerNote)
lnwire.SetOptFromMap(tm, &inv.InvreqPaths, invreqPaths)
lnwire.SetOptFromMap(tm, &inv.InvreqBip353Name, bip353)
lnwire.SetOptFromMap(tm, &inv.InvoicePaths, invPaths)
lnwire.SetOptFromMap(tm, &inv.InvoiceBlindedPay, blindedPay)
lnwire.SetOptFromMap(tm, &inv.InvoiceCreatedAt, createdAt)
lnwire.SetOptFromMap(tm, &inv.InvoiceRelativeExp, relExp)
lnwire.SetOptFromMap(tm, &inv.InvoicePaymentHash, payHash)
lnwire.SetOptFromMap(tm, &inv.InvoiceAmount, invAmt)
lnwire.SetOptFromMap(tm, &inv.InvoiceFallbacks, fallbacks)
lnwire.SetOptFromMap(tm, &inv.InvoiceFeatures, invFeat)
lnwire.SetOptFromMap(tm, &inv.InvoiceNodeID, nodeID)
lnwire.SetOptFromMap(tm, &inv.Signature, sig)
inv.decodedTLVs = tm
return &inv, nil
}
// NewInvoiceFromRequest constructs a new Invoice by copying (mirroring) all
// non-signature fields from the provided InvoiceRequest. When invreq_amount is
// present it is mirrored into invoice_amount per the writer requirement. The
// caller is responsible for populating the remaining invoice-specific fields
// (invoice_created_at, invoice_payment_hash, invoice_node_id, invoice_paths,
// invoice_blindedpay, ...) and signing the invoice.
func NewInvoiceFromRequest(req *InvoiceRequest) *Invoice {
inv := &Invoice{
InvreqMetadata: req.InvreqMetadata,
OfferChains: req.OfferChains,
OfferMetadata: req.OfferMetadata,
OfferCurrency: req.OfferCurrency,
OfferAmount: req.OfferAmount,
OfferDescription: req.OfferDescription,
OfferFeatures: req.OfferFeatures,
OfferAbsoluteExpiry: req.OfferAbsoluteExpiry,
OfferPaths: req.OfferPaths,
OfferIssuer: req.OfferIssuer,
OfferQuantityMax: req.OfferQuantityMax,
OfferIssuerID: req.OfferIssuerID,
InvreqChain: req.InvreqChain,
InvreqAmount: req.InvreqAmount,
InvreqFeatures: req.InvreqFeatures,
InvreqQuantity: req.InvreqQuantity,
InvreqPayerID: req.InvreqPayerID,
InvreqPayerNote: req.InvreqPayerNote,
InvreqPaths: req.InvreqPaths,
InvreqBip353Name: req.InvreqBip353Name,
// Carry the request's unknown signed-range TLVs. Known invreq
// types appear in the map with nil values and are skipped when
// the sidecar is merged, so this re-emits only the unknowns and
// never duplicates the typed fields copied above. Any
// signature-range entries (240-1000) cloned here are inert:
// allRecordsFromTypeMap drops them via bolt12InUnsignedRange,
// so the request's signature never leaks into the invoice.
decodedTLVs: maps.Clone(req.decodedTLVs),
}
// Writer rule: if invreq_amount is present, invoice_amount MUST be set
// to it. When absent, the caller sets the expected amount.
req.InvreqAmount.WhenSome(
func(r tlv.RecordT[tlv.TlvType82, TUint64]) {
inv.InvoiceAmount = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType170, TUint64](r.Val),
)
},
)
return inv
}

113
bolt12/invoice_error.go Normal file
View file

@ -0,0 +1,113 @@
package bolt12
import (
"fmt"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// InvoiceError represents a BOLT 12 invoice_error message, the negative reply a
// node sends when it rejects an invoice_request or a returned invoice.
type InvoiceError struct {
// ErroneousField names the TLV type in the rejected message that caused
// the failure, letting the recipient pinpoint what to change.
ErroneousField tlv.OptionalRecordT[tlv.TlvType1, TUint64]
// SuggestedValue provides a valid replacement for the erroneous field.
// MUST NOT be set if ErroneousField is absent.
SuggestedValue tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob]
// Error is a UTF-8 string explaining the rejection. Required by the
// spec.
Error tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
// decodedTLVs holds every wire TLV type, including unknown ones, so
// ValidateInvoiceErrorRead can apply the must-understand rule.
decodedTLVs tlv.TypeMap
}
// allRecordProducers returns record producers for every set optional field, in
// declaration order.
func (ie *InvoiceError) allRecordProducers() []tlv.RecordProducer {
var p []tlv.RecordProducer
lnwire.AddOpt(&p, ie.ErroneousField)
lnwire.AddOpt(&p, ie.SuggestedValue)
lnwire.AddOpt(&p, ie.Error)
return p
}
// Encode validates the invoice error per writer requirements and serialises it
// into a TLV byte stream suitable for embedding in an onion message payload at
// type 68. Note that Encode intentionally drops any unknown TLVs. Since
// invoice_error does not carry a cryptographic signature, there is no
// signature to invalidate by dropping unrecognized TLVs (unlike signed
// messages such as invoices, where unknown TLVs must be preserved to keep
// signatures valid).
func (ie *InvoiceError) Encode() ([]byte, error) {
if err := ValidateInvoiceErrorWrite(ie); err != nil {
return nil, fmt.Errorf("validate invoice error: %w", err)
}
records := lnwire.ProduceRecordsSorted(ie.allRecordProducers()...)
return lnwire.EncodeRecords(records)
}
// DecodeInvoiceError deserializes an invoice error from a TLV byte stream (the
// raw value of onion message payload type 68). Decoding is permissive. Run
// ValidateInvoiceErrorRead for the BOLT 1 must-understand check.
func DecodeInvoiceError(data []byte) (*InvoiceError, error) {
var ie InvoiceError
errField := tlv.ZeroRecordT[tlv.TlvType1, TUint64]()
sugVal := tlv.ZeroRecordT[tlv.TlvType3, tlv.Blob]()
errMsg := tlv.ZeroRecordT[tlv.TlvType5, tlv.Blob]()
tm, err := decodeStream(
data,
errField.Record(),
sugVal.Record(),
errMsg.Record(),
)
if err != nil {
return nil, fmt.Errorf("decode invoice error: %w", err)
}
lnwire.SetOptFromMap(tm, &ie.ErroneousField, errField)
lnwire.SetOptFromMap(tm, &ie.SuggestedValue, sugVal)
lnwire.SetOptFromMap(tm, &ie.Error, errMsg)
ie.decodedTLVs = tm
return &ie, nil
}
// ErrorMessage returns the decoded error string, or empty if not set. The bytes
// originate from a remote peer over an onion message and are not sanitised
// here, so callers must scrub them before logging or display.
func (ie *InvoiceError) ErrorMessage() string {
var msg []byte
ie.Error.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
msg = r.Val
})
return string(msg)
}
// FieldNumber returns the erroneous field number, if set.
func (ie *InvoiceError) FieldNumber() (uint64, bool) {
var (
val uint64
ok bool
)
ie.ErroneousField.WhenSome(
func(r tlv.RecordT[tlv.TlvType1, TUint64]) {
val = uint64(r.Val)
ok = true
},
)
return val, ok
}

View file

@ -0,0 +1,287 @@
package bolt12
import (
"testing"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
// someErrField builds a set erroneous_field record for the given field
// number, keeping the test tables compact.
func someErrField(n uint64) tlv.OptionalRecordT[tlv.TlvType1, TUint64] {
return tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType1](TUint64(n)),
)
}
// someSuggested builds a set suggested_value record from raw bytes.
func someSuggested(b tlv.Blob) tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob] {
return tlv.SomeRecordT(tlv.NewPrimitiveRecord[tlv.TlvType3](b))
}
// someError builds a set error record from a string.
func someError(s string) tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] {
return tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5](tlv.Blob(s)),
)
}
// TestInvoiceErrorRoundTrip verifies that encoding an invoice_error and
// decoding the result recovers every field, and that re-encoding the decoded
// message reproduces the original bytes, both for a fully-populated message
// and for the minimal error-only case. Note that this round-trip property
// only guarantees exact byte reproducibility for messages containing only
// known/declared fields; any unknown fields present in decoded messages are
// intentionally dropped when re-encoded.
func TestInvoiceErrorRoundTrip(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ie *InvoiceError
wantMsg string
wantHasField bool
wantFieldNum uint64
wantSuggest []byte
}{
{
name: "all fields",
ie: &InvoiceError{
ErroneousField: someErrField(82),
SuggestedValue: someSuggested(
[]byte{0x00, 0x01, 0x86, 0xa0},
),
Error: someError("amount too low"),
},
wantMsg: "amount too low",
wantHasField: true,
wantFieldNum: 82,
wantSuggest: []byte{0x00, 0x01, 0x86, 0xa0},
},
{
name: "minimal error only",
ie: &InvoiceError{
Error: someError("rejected"),
},
wantMsg: "rejected",
wantHasField: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
encoded, err := tc.ie.Encode()
require.NoError(t, err)
require.NotEmpty(t, encoded)
decoded, err := DecodeInvoiceError(encoded)
require.NoError(t, err)
require.Equal(t, tc.wantMsg, decoded.ErrorMessage())
fieldNum, ok := decoded.FieldNumber()
require.Equal(t, tc.wantHasField, ok)
if tc.wantHasField {
require.Equal(t, tc.wantFieldNum, fieldNum)
}
var sugVal []byte
decoded.SuggestedValue.WhenSome(
func(r tlv.RecordT[tlv.TlvType3, tlv.Blob]) {
sugVal = r.Val
},
)
require.Equal(t, tc.wantSuggest, sugVal)
// Re-encoding the decoded message must reproduce the
// original bytes, pinning canonical record ordering.
reencoded, err := decoded.Encode()
require.NoError(t, err)
require.Equal(t, encoded, reencoded)
})
}
}
// TestInvoiceErrorRoundTripWithUnknown verifies that decoding an invoice_error
// containing unknown odd fields works, but re-encoding the decoded structure
// drops those unknown fields, yielding only the known fields in the encoded
// byte stream.
func TestInvoiceErrorRoundTripWithUnknown(t *testing.T) {
t.Parallel()
// Create a valid invoice_error with only known fields and encode it.
ie := &InvoiceError{
Error: someError("rejected with unknown field present"),
}
valid, err := ie.Encode()
require.NoError(t, err)
// Append an unknown odd TLV (type 7) to the valid TLV stream.
// 0x07 (type), 0x02 (length), 0xaa, 0xbb (value).
streamWithUnknown := append(
append([]byte{}, valid...), 0x07, 0x02, 0xaa, 0xbb,
)
// Decode the stream. It should succeed because unknown odd fields are
// ignored/tolerated.
decoded, err := DecodeInvoiceError(streamWithUnknown)
require.NoError(t, err)
require.Equal(
t, "rejected with unknown field present",
decoded.ErrorMessage(),
)
// Re-encode the decoded message.
reencoded, err := decoded.Encode()
require.NoError(t, err)
// The re-encoded stream must drop the unknown type 7 field, recovering
// exactly the 'valid' bytes, rather than 'streamWithUnknown'.
require.Equal(t, valid, reencoded)
}
// TestInvoiceErrorEncodeValidates verifies that Encode runs the writer
// validation before serialising, so an invalid invoice_error never reaches the
// wire.
func TestInvoiceErrorEncodeValidates(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ie *InvoiceError
wantErr error
}{
{
name: "missing error",
ie: &InvoiceError{},
wantErr: ErrMissingError,
},
{
name: "empty error",
ie: &InvoiceError{Error: someError("")},
wantErr: ErrEmptyError,
},
{
name: "non-utf8 error",
ie: &InvoiceError{
Error: someError(string([]byte{0xff, 0xfe})),
},
wantErr: ErrInvalidUTF8,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := tc.ie.Encode()
require.ErrorIs(t, err, tc.wantErr)
})
}
}
// TestDecodeInvoiceError verifies decode-level behavior: a truncated stream
// errors, and an unknown odd TLV trailing a valid message is tolerated per the
// BOLT rule that unknown odd types may be ignored.
func TestDecodeInvoiceError(t *testing.T) {
t.Parallel()
valid, err := (&InvoiceError{Error: someError("rejected")}).Encode()
require.NoError(t, err)
// A valid message with an unknown odd TLV (type 7) appended after error
// (type 5), kept in ascending type order.
withOdd := append(append([]byte{}, valid...), 0x07, 0x02, 0xaa, 0xbb)
tests := []struct {
name string
data []byte
wantErr bool
wantMsg string
}{
{
// Type 5 (error) claims length 16 but supplies one
// byte.
name: "truncated",
data: []byte{0x05, 0x10, 0x01},
wantErr: true,
},
{
name: "unknown odd tolerated",
data: withOdd,
wantMsg: "rejected",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
decoded, err := DecodeInvoiceError(tc.data)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.wantMsg, decoded.ErrorMessage())
})
}
}
// TestValidateInvoiceErrorRead verifies the BOLT 1 must-understand rule: an
// unknown even TLV is rejected (including a zero-length one, which still
// occupies a type slot), while an unknown odd TLV is tolerated.
func TestValidateInvoiceErrorRead(t *testing.T) {
t.Parallel()
// A valid encoded invoice_error (error = "rejected", type 5). Trailers
// use types > 5 to keep the stream strictly increasing.
base, err := (&InvoiceError{Error: someError("rejected")}).Encode()
require.NoError(t, err)
tests := []struct {
name string
trailer []byte
wantErr error
}{
{
name: "known only",
},
{
name: "unknown odd tolerated",
trailer: []byte{0x07, 0x02, 0xaa, 0xbb},
},
{
name: "unknown even rejected",
trailer: []byte{0x06, 0x02, 0xaa, 0xbb},
wantErr: ErrUnknownEvenType,
},
{
name: "unknown even zero-length rejected",
trailer: []byte{0x06, 0x00},
wantErr: ErrUnknownEvenType,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
stream := append(
append([]byte{}, base...), tc.trailer...,
)
decoded, err := DecodeInvoiceError(stream)
require.NoError(t, err)
err = ValidateInvoiceErrorRead(decoded)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
})
}
}

302
bolt12/invoice_request.go Normal file
View file

@ -0,0 +1,302 @@
package bolt12
import (
"bytes"
"errors"
"fmt"
"maps"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
var (
// ErrMissingPayerID is returned when invreq_payer_id is absent.
ErrMissingPayerID = errors.New("missing invreq_payer_id")
// ErrMissingMetadata is returned when invreq_metadata is absent.
ErrMissingMetadata = errors.New("missing invreq_metadata")
)
// InvoiceRequest represents a BOLT 12 invoice_request message. It mirrors offer
// fields from the original offer. It also adds payer-specific fields and a
// Schnorr signature.
//
// An invoice request should be constructed from an offer (e.g., using
// NewInvoiceRequestFromOffer) unless it is a spontaneous invoice request.
type InvoiceRequest struct {
// OfferChains are the chains that the mirrored offer is valid for.
OfferChains tlv.OptionalRecordT[tlv.TlvType2, ChainsRecord]
// OfferMetadata is the metadata from the mirrored offer.
OfferMetadata tlv.OptionalRecordT[tlv.TlvType4, tlv.Blob]
// OfferCurrency is the currency from the mirrored offer.
OfferCurrency tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob]
// OfferAmount is the amount from the mirrored offer.
OfferAmount tlv.OptionalRecordT[tlv.TlvType8, TUint64]
// OfferDescription is the description from the mirrored offer.
OfferDescription tlv.OptionalRecordT[tlv.TlvType10, tlv.Blob]
// OfferFeatures are the features required by the mirrored offer.
OfferFeatures tlv.OptionalRecordT[
tlv.TlvType12, lnwire.RawFeatureVector,
]
// OfferAbsoluteExpiry is the absolute expiry from the mirrored offer.
OfferAbsoluteExpiry tlv.OptionalRecordT[tlv.TlvType14, TUint64]
// OfferPaths are the blinded paths from the mirrored offer.
OfferPaths tlv.OptionalRecordT[tlv.TlvType16, lnwire.BlindedPaths]
// OfferIssuer is the issuer name from the mirrored offer.
OfferIssuer tlv.OptionalRecordT[tlv.TlvType18, tlv.Blob]
// OfferQuantityMax is the maximum quantity allowed by the mirrored
// offer.
OfferQuantityMax tlv.OptionalRecordT[tlv.TlvType20, TUint64]
// OfferIssuerID is the public key of the offer issuer.
OfferIssuerID tlv.OptionalRecordT[tlv.TlvType22, *btcec.PublicKey]
// InvreqMetadata is a blob of unpredictable bytes provided by the
// payer. It serves multiple roles under the BOLT 12 specification:
// 1. Nonce: Ensures cryptographic signature entropy.
// 2. Idempotency Key: If the metadata is identical to a previous
// request, the receiver may reply with the previously generated
// invoice instead of minting a new one.
// 3. Message Marker: As Type 0, it canonically sits at the start of
// the invoice_request TLV stream.
InvreqMetadata tlv.OptionalRecordT[tlv.TlvType0, tlv.Blob]
// InvreqChain is the chain that the payer is using for this request.
InvreqChain tlv.OptionalRecordT[tlv.TlvType80, [32]byte]
// InvreqAmount is the amount the payer is offering to pay.
InvreqAmount tlv.OptionalRecordT[tlv.TlvType82, TUint64]
// InvreqFeatures are the features provided by the payer.
InvreqFeatures tlv.OptionalRecordT[
tlv.TlvType84, lnwire.RawFeatureVector,
]
// InvreqQuantity is the quantity of the offer item being requested.
InvreqQuantity tlv.OptionalRecordT[tlv.TlvType86, TUint64]
// InvreqPayerID is the public key the payer uses to sign the request.
InvreqPayerID tlv.OptionalRecordT[tlv.TlvType88, *btcec.PublicKey]
// InvreqPayerNote is an optional note from the payer.
InvreqPayerNote tlv.OptionalRecordT[tlv.TlvType89, tlv.Blob]
// InvreqPaths are the blinded paths the payer wants the invoice to be
// sent to.
InvreqPaths tlv.OptionalRecordT[tlv.TlvType90, lnwire.BlindedPaths]
// InvreqBip353Name is the BIP 353 name of the payer.
InvreqBip353Name tlv.OptionalRecordT[tlv.TlvType91, tlv.Blob]
// Signature is a BIP-340 Schnorr signature covering all fields.
Signature tlv.OptionalRecordT[tlv.TlvType240, [64]byte]
// decodedTLVs is the canonical TypeMap produced by the typed- stream
// pass that decoded this request. See Offer.decodedTLVs for the design
// rationale.
decodedTLVs tlv.TypeMap
}
// AllRecords returns the canonical sorted record list for this invoice request,
// merging the typed records with any extra signed-range fields that the decoder
// preserved.
//
// NOTE: this is part of the tlv.PureTLVMessage interface.
func (ir *InvoiceRequest) AllRecords() []tlv.Record {
return allRecordsFromTypeMap(
ir.allRecordProducers(), ir.decodedTLVs,
)
}
var _ lnwire.PureTLVMessage = (*InvoiceRequest)(nil)
// allRecordProducers returns the set of records that are present.
func (ir *InvoiceRequest) allRecordProducers() []tlv.RecordProducer {
var p []tlv.RecordProducer
lnwire.AddOpt(&p, ir.InvreqMetadata)
lnwire.AddOpt(&p, ir.OfferChains)
lnwire.AddOpt(&p, ir.OfferMetadata)
lnwire.AddOpt(&p, ir.OfferCurrency)
lnwire.AddOpt(&p, ir.OfferAmount)
lnwire.AddOpt(&p, ir.OfferDescription)
lnwire.AddOpt(&p, ir.OfferFeatures)
lnwire.AddOpt(&p, ir.OfferAbsoluteExpiry)
lnwire.AddOpt(&p, ir.OfferPaths)
lnwire.AddOpt(&p, ir.OfferIssuer)
lnwire.AddOpt(&p, ir.OfferQuantityMax)
lnwire.AddOpt(&p, ir.OfferIssuerID)
lnwire.AddOpt(&p, ir.InvreqChain)
lnwire.AddOpt(&p, ir.InvreqAmount)
lnwire.AddOpt(&p, ir.InvreqFeatures)
lnwire.AddOpt(&p, ir.InvreqQuantity)
lnwire.AddOpt(&p, ir.InvreqPayerID)
lnwire.AddOpt(&p, ir.InvreqPayerNote)
lnwire.AddOpt(&p, ir.InvreqPaths)
lnwire.AddOpt(&p, ir.InvreqBip353Name)
lnwire.AddOpt(&p, ir.Signature)
return p
}
// Encode validates the invoice request per writer requirements and serialises
// it via the PureTLVMessage shape.
func (ir *InvoiceRequest) Encode() ([]byte, error) {
if err := ValidateInvoiceRequestWrite(ir); err != nil {
return nil, fmt.Errorf("validate invoice request: %w", err)
}
var buf bytes.Buffer
if err := lnwire.EncodePureTLVMessage(ir, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecodeInvoiceRequest deserializes an invoice request from a TLV byte stream.
// Decoding is permissive: callers that need spec compliance must run
// ValidateInvoiceRequestRead.
func DecodeInvoiceRequest(data []byte) (*InvoiceRequest, error) {
var ir InvoiceRequest
invreqMetadata := tlv.ZeroRecordT[tlv.TlvType0, tlv.Blob]()
chains := tlv.ZeroRecordT[tlv.TlvType2, ChainsRecord]()
metadata := tlv.ZeroRecordT[tlv.TlvType4, tlv.Blob]()
currency := tlv.ZeroRecordT[tlv.TlvType6, tlv.Blob]()
amount := tlv.ZeroRecordT[tlv.TlvType8, TUint64]()
desc := tlv.ZeroRecordT[tlv.TlvType10, tlv.Blob]()
features := tlv.ZeroRecordT[tlv.TlvType12, lnwire.RawFeatureVector]()
expiry := tlv.ZeroRecordT[tlv.TlvType14, TUint64]()
paths := tlv.ZeroRecordT[tlv.TlvType16, lnwire.BlindedPaths]()
issuer := tlv.ZeroRecordT[tlv.TlvType18, tlv.Blob]()
qtyMax := tlv.ZeroRecordT[tlv.TlvType20, TUint64]()
issuerID := tlv.ZeroRecordT[tlv.TlvType22, *btcec.PublicKey]()
invreqChain := tlv.ZeroRecordT[tlv.TlvType80, [32]byte]()
invreqAmount := tlv.ZeroRecordT[tlv.TlvType82, TUint64]()
invreqFeatures := tlv.ZeroRecordT[
tlv.TlvType84, lnwire.RawFeatureVector,
]()
invreqQty := tlv.ZeroRecordT[tlv.TlvType86, TUint64]()
payerID := tlv.ZeroRecordT[tlv.TlvType88, *btcec.PublicKey]()
payerNote := tlv.ZeroRecordT[tlv.TlvType89, tlv.Blob]()
invreqPaths := tlv.ZeroRecordT[tlv.TlvType90, lnwire.BlindedPaths]()
bip353 := tlv.ZeroRecordT[tlv.TlvType91, tlv.Blob]()
sig := tlv.ZeroRecordT[tlv.TlvType240, [64]byte]()
tm, err := decodeStream(
data, invreqMetadata.Record(), chains.Record(),
metadata.Record(), currency.Record(), amount.Record(),
desc.Record(), features.Record(), expiry.Record(),
paths.Record(), issuer.Record(), qtyMax.Record(),
issuerID.Record(), invreqChain.Record(), invreqAmount.Record(),
invreqFeatures.Record(), invreqQty.Record(), payerID.Record(),
payerNote.Record(), invreqPaths.Record(), bip353.Record(),
sig.Record(),
)
if err != nil {
return nil, fmt.Errorf("decode invoice request: %w", err)
}
lnwire.SetOptFromMap(tm, &ir.InvreqMetadata, invreqMetadata)
lnwire.SetOptFromMap(tm, &ir.OfferChains, chains)
lnwire.SetOptFromMap(tm, &ir.OfferMetadata, metadata)
lnwire.SetOptFromMap(tm, &ir.OfferCurrency, currency)
lnwire.SetOptFromMap(tm, &ir.OfferAmount, amount)
lnwire.SetOptFromMap(tm, &ir.OfferDescription, desc)
lnwire.SetOptFromMap(tm, &ir.OfferFeatures, features)
lnwire.SetOptFromMap(tm, &ir.OfferAbsoluteExpiry, expiry)
lnwire.SetOptFromMap(tm, &ir.OfferPaths, paths)
lnwire.SetOptFromMap(tm, &ir.OfferIssuer, issuer)
lnwire.SetOptFromMap(tm, &ir.OfferQuantityMax, qtyMax)
lnwire.SetOptFromMap(tm, &ir.OfferIssuerID, issuerID)
lnwire.SetOptFromMap(tm, &ir.InvreqChain, invreqChain)
lnwire.SetOptFromMap(tm, &ir.InvreqAmount, invreqAmount)
lnwire.SetOptFromMap(tm, &ir.InvreqFeatures, invreqFeatures)
lnwire.SetOptFromMap(tm, &ir.InvreqQuantity, invreqQty)
lnwire.SetOptFromMap(tm, &ir.InvreqPayerID, payerID)
lnwire.SetOptFromMap(tm, &ir.InvreqPayerNote, payerNote)
lnwire.SetOptFromMap(tm, &ir.InvreqPaths, invreqPaths)
lnwire.SetOptFromMap(tm, &ir.InvreqBip353Name, bip353)
lnwire.SetOptFromMap(tm, &ir.Signature, sig)
ir.decodedTLVs = tm
return &ir, nil
}
// NewInvoiceRequestFromOffer constructs a new InvoiceRequest by copying
// (mirroring) all fields from the provided Offer. It assigns the payer ID and
// payer metadata; the caller should subsequently sign the request.
//
// Per "MUST copy all fields from the offer (including unknown fields)", the
// offer's unknown TLVs are carried via the decodedTLVs sidecar so they are
// signed and mirrored into the invoice. Note that because unknown even TLV
// types in the offer would have already been rejected by ValidateOfferRead, any
// unknown TLVs mirrored here are guaranteed to be unknown odd TLVs ("it's ok to
// be odd") which are safe to ignore and carry forward.
//
// chain is the genesis hash the payer intends to pay on. invreq_chain is set
// only when chain is not Bitcoin mainnet (absent defaults to mainnet); writer
// validation enforces that it is one of the offer's chains.
func NewInvoiceRequestFromOffer(offer *Offer, payerID *btcec.PublicKey,
metadata []byte, chain [32]byte) (*InvoiceRequest, error) {
if payerID == nil {
return nil, ErrMissingPayerID
}
if len(metadata) == 0 {
return nil, ErrMissingMetadata
}
ir := &InvoiceRequest{
OfferChains: offer.OfferChains,
OfferMetadata: offer.OfferMetadata,
OfferCurrency: offer.OfferCurrency,
OfferAmount: offer.OfferAmount,
OfferDescription: offer.OfferDescription,
OfferFeatures: offer.OfferFeatures,
OfferAbsoluteExpiry: offer.OfferAbsoluteExpiry,
OfferPaths: offer.OfferPaths,
OfferIssuer: offer.OfferIssuer,
OfferQuantityMax: offer.OfferQuantityMax,
OfferIssuerID: offer.OfferIssuerID,
InvreqPayerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType88](payerID),
),
InvreqMetadata: tlv.SomeRecordT(
tlv.RecordT[tlv.TlvType0, tlv.Blob]{
Val: metadata,
},
),
// Carry the offer's unknown signed-range TLVs. Known offer
// types appear in the map with nil values and are skipped when
// the sidecar is merged, so this re-emits only the unknowns and
// never duplicates the typed fields copied above.
decodedTLVs: maps.Clone(offer.decodedTLVs),
}
// Set invreq_chain only for non-bitcoin chains; for bitcoin mainnet the
// spec says SHOULD omit, and an absent invreq_chain defaults back to
// mainnet on the read side.
if chain != bitcoinMainnetGenesisHash {
ir.InvreqChain = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType80, [32]byte](chain),
)
}
return ir, nil
}

View file

@ -0,0 +1,171 @@
package bolt12
import (
"bytes"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
// TestInvoiceRequestRoundTrip pins encode→decode→re-encode for an
// InvoiceRequest with a representative subset of optional fields.
func TestInvoiceRequestRoundTrip(t *testing.T) {
t.Parallel()
_, bobPub := bobKey()
metadata := tlv.Blob("payer-metadata")
ir := &InvoiceRequest{
OfferDescription: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](
tlv.Blob("description"),
),
),
InvreqPayerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType88](bobPub),
),
InvreqMetadata: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType0](metadata),
),
InvreqAmount: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType82, TUint64](1000),
),
Signature: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType240](
[64]byte{0x01},
),
),
}
encoded, err := ir.Encode()
require.NoError(t, err)
require.NotEmpty(t, encoded)
decoded, err := DecodeInvoiceRequest(encoded)
require.NoError(t, err)
require.Equal(
t, bobPub.SerializeCompressed(),
decoded.InvreqPayerID.UnwrapOrFailV(t).SerializeCompressed(),
)
require.Equal(t, metadata, decoded.InvreqMetadata.UnwrapOrFailV(t))
require.Equal(
t, TUint64(1000), decoded.InvreqAmount.UnwrapOrFailV(t),
)
reencoded, err := decoded.Encode()
require.NoError(t, err)
require.Equal(t, encoded, reencoded)
}
// TestNewInvoiceRequestFromOffer tests the constructor for mirroring all offer
// fields and properly assigning the payer ID and metadata.
func TestNewInvoiceRequestFromOffer(t *testing.T) {
t.Parallel()
offer := validBobOffer(t)
// Add some optional offer fields for mirroring verification.
offer.OfferDescription = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](tlv.Blob("description")),
)
offer.OfferAmount = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType8, TUint64](5000),
)
priv, err := btcec.NewPrivateKey()
require.NoError(t, err)
payerID := priv.PubKey()
metadata := []byte("payer-metadata")
ir, err := NewInvoiceRequestFromOffer(
offer, payerID, metadata, bitcoinMainnetGenesisHash,
)
require.NoError(t, err)
require.NotNil(t, ir)
// Verify offer fields are copied exactly
require.Equal(t, offer.OfferIssuerID, ir.OfferIssuerID)
require.Equal(t, offer.OfferDescription, ir.OfferDescription)
require.Equal(t, offer.OfferAmount, ir.OfferAmount)
// Verify payer ID and metadata are set correctly
require.Equal(t, payerID, ir.InvreqPayerID.UnwrapOrFailV(t))
require.Equal(t, metadata, ir.InvreqMetadata.UnwrapOrFailV(t))
// For Bitcoin mainnet the spec says SHOULD omit invreq_chain.
require.False(t, ir.InvreqChain.IsSome())
// A non-bitcoin chain must be set explicitly so it does not default
// back to mainnet on the read side.
var altChain [32]byte
for i := range altChain {
altChain[i] = 0xab
}
irAlt, err := NewInvoiceRequestFromOffer(
offer, payerID, metadata, altChain,
)
require.NoError(t, err)
require.Equal(t, altChain, irAlt.InvreqChain.UnwrapOrFailV(t))
}
// TestNewInvoiceRequestFromOfferMirrorsUnknownFields verifies the writer
// requirement "MUST copy all fields from the offer (including unknown fields)":
// an unknown odd TLV in the offer's signed range must survive into the
// constructed request's record set so it is signed and later mirrored into the
// invoice.
func TestNewInvoiceRequestFromOfferMirrorsUnknownFields(t *testing.T) {
t.Parallel()
_, pub := bobKey()
// Build a minimal valid offer, encode it, then splice in an unknown odd
// TLV (type 33, within the offer signed range) and decode it
// back so the unknown lands in the offer's decodedTLVs sidecar.
offer := &Offer{
OfferDescription: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](tlv.Blob("desc")),
),
OfferIssuerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType22](pub),
),
}
encoded, err := offer.Encode()
require.NoError(t, err)
const unknownType = 33
unknownVal := []byte("xyz")
var extra bytes.Buffer
require.NoError(t, tlv.WriteVarInt(&extra, unknownType, &[8]byte{}))
require.NoError(t, tlv.WriteVarInt(
&extra, uint64(len(unknownVal)), &[8]byte{},
))
extra.Write(unknownVal)
// TLV records are canonically ordered by type; type 33 sorts after the
// offer's existing types (10, 22), so appending keeps the stream
// sorted.
spliced := append(append([]byte{}, encoded...), extra.Bytes()...)
decodedOffer, err := decodeOffer(spliced)
require.NoError(t, err)
ir, err := NewInvoiceRequestFromOffer(
decodedOffer, pub, []byte("metadata"),
bitcoinMainnetGenesisHash,
)
require.NoError(t, err)
// The unknown field must appear in the request's canonical record set.
var found bool
for _, r := range ir.AllRecords() {
if r.Type() == unknownType {
found = true
}
}
require.True(t, found, "unknown offer TLV not mirrored into request")
}

359
bolt12/invoice_test.go Normal file
View file

@ -0,0 +1,359 @@
package bolt12
import (
"bytes"
"testing"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
// validInvoice returns an Invoice populated with the minimum set of fields
// required to satisfy ValidateInvoiceWrite.
func validInvoice(t *testing.T) *Invoice {
t.Helper()
_, pub := bobKey()
var payHash [32]byte
for i := range payHash {
payHash[i] = byte(i)
}
_, intro := aliceKey()
_, blinding := bobKey()
_, hopPub := aliceKey()
introNode, err := lnwire.NewPubkeyIntro(intro)
require.NoError(t, err)
return &Invoice{
InvoiceCreatedAt: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType164, TUint64](
TUint64(1234567890),
),
),
InvoiceAmount: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType170, TUint64](
TUint64(100_000),
),
),
InvoicePaymentHash: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType168, [32]byte](
payHash,
),
),
InvoiceNodeID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType176](pub),
),
InvoicePaths: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType160, lnwire.BlindedPaths](
lnwire.BlindedPaths{
Paths: []lnwire.BlindedPath{{
IntroductionNode: introNode,
BlindingPoint: blinding,
Hops: []lnwire.BlindedHop{{
BlindedNodeID: hopPub,
}},
}},
},
),
),
InvoiceBlindedPay: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType162, BlindedPayInfos](
BlindedPayInfos{Infos: []BlindedPayInfo{{}}},
),
),
}
}
// TestUsableFallbackAddresses pins the BOLT 12 ignore semantics for
// invoice_fallbacks.
func TestUsableFallbackAddresses(t *testing.T) {
t.Parallel()
addrs := []FallbackAddress{
// Valid: version 0, 2 bytes.
{Version: 0, Address: []byte{0x01, 0x02}},
// Invalid: version 17, 2 bytes. Version is not supported.
{Version: 17, Address: []byte{0x01, 0x02}},
// Invalid: version 0, 1 byte. Address is too short.
{Version: 0, Address: []byte{0x01}},
// Invalid: version 0, 41 bytes. Address is too long.
{Version: 0, Address: make([]byte, 41)},
// Valid: version 16, 40 bytes.
{Version: 16, Address: make([]byte, 40)},
}
inv := &Invoice{
InvoiceFallbacks: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType172, FallbackAddresses](
FallbackAddresses{Addrs: addrs},
),
),
}
got := inv.UsableFallbackAddresses()
require.Len(t, got, 2)
require.Equal(t, byte(0), got[0].Version)
require.Equal(t, byte(16), got[1].Version)
require.Len(t, got[1].Address, 40)
}
// TestUsablePaths pins the BOLT 12 reader filter that excludes any blinded path
// whose payinfo.features carries an unknown required (even) bit, and confirms
// each surviving entry is paired with its own payinfo by index.
func TestUsablePaths(t *testing.T) {
t.Parallel()
_, blinding := bobKey()
_, hopPub := aliceKey()
_, intro := aliceKey()
introNode, err := lnwire.NewPubkeyIntro(intro)
require.NoError(t, err)
// hop builds a minimal single-hop blinded path; two of these populate
// invoice_paths so the by-index pairing with payinfos can be observed.
hop := lnwire.BlindedPath{
IntroductionNode: introNode,
BlindingPoint: blinding,
Hops: []lnwire.BlindedHop{{BlindedNodeID: hopPub}},
}
pathsRecord := func(n int) tlv.OptionalRecordT[
tlv.TlvType160, lnwire.BlindedPaths,
] {
paths := make([]lnwire.BlindedPath, n)
for i := range paths {
paths[i] = hop
}
return tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType160, lnwire.BlindedPaths](
lnwire.BlindedPaths{Paths: paths},
),
)
}
payRecord := func(infos ...BlindedPayInfo) tlv.OptionalRecordT[
tlv.TlvType162, BlindedPayInfos,
] {
return tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType162, BlindedPayInfos](
BlindedPayInfos{Infos: infos},
),
)
}
// The first payinfo carries an unknown required feature bit
// (MPPRequired); the second is featureless.
required := *lnwire.NewRawFeatureVector(lnwire.MPPRequired)
inv := &Invoice{
InvoicePaths: pathsRecord(2),
InvoiceBlindedPay: payRecord(
BlindedPayInfo{FeeBaseMsat: 1, Features: required},
BlindedPayInfo{FeeBaseMsat: 2},
),
}
// Empty catalogue: the MPPRequired bit is unknown, so path 0 is
// filtered out and only path 1 (fee_base 2) survives.
got := inv.UsablePaths(nil)
require.Len(t, got, 1)
require.Equal(t, uint32(2), got[0].PayInfo.FeeBaseMsat)
// Once the bit is known, both paths become usable and stay paired with
// their own payinfo in order.
known := map[lnwire.FeatureBit]string{lnwire.MPPRequired: "mpp"}
got = inv.UsablePaths(known)
require.Len(t, got, 2)
require.Equal(t, uint32(1), got[0].PayInfo.FeeBaseMsat)
require.Equal(t, uint32(2), got[1].PayInfo.FeeBaseMsat)
// A length mismatch between paths and payinfos yields no usable paths
// (rejected upstream by ValidateInvoiceRead).
inv.InvoiceBlindedPay = payRecord(BlindedPayInfo{})
require.Empty(t, inv.UsablePaths(known))
}
// TestInvoiceRoundTripPreservesAllTypes encodes a fully populated invoice then
// decodes it back, asserting every field is preserved byte-for-byte. The codec
// promises bijection on the message level, and any drift (dropped record,
// re-ordered output) breaks downstream signature verification because the
// Merkle root depends on the exact raw TLV stream.
func TestInvoiceRoundTripPreservesAllTypes(t *testing.T) {
t.Parallel()
inv := validInvoice(t)
inv.Signature = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType240, [64]byte]([64]byte{}),
)
encoded, err := inv.Encode()
require.NoError(t, err)
require.NotEmpty(t, encoded)
decoded, err := DecodeInvoice(encoded)
require.NoError(t, err)
err = ValidateInvoiceRead(decoded, bitcoinMainnetGenesisHash,
InvoiceFeatureCatalogues{})
require.NoError(t, err)
// Re-encode the decoded copy and confirm canonicality.
// decode(encode(decode(encode(x)))) must equal decode(encode(x)).
encoded2, err := decoded.Encode()
require.NoError(t, err)
require.Equal(t, encoded, encoded2)
}
// TestDecodeInvoiceRejectsTruncated locks in that DecodeInvoice surfaces an
// error when fed a truncated TLV stream rather than returning a partial
// Invoice. A silent partial-decode would let validation see fields that weren't
// actually on the wire.
func TestDecodeInvoiceRejectsTruncated(t *testing.T) {
t.Parallel()
inv := validInvoice(t)
encoded, err := inv.Encode()
require.NoError(t, err)
// Chop off the last byte. The truncation lands in the middle of the
// final blinded_pay record's variable-length payload.
truncated := encoded[:len(encoded)-1]
_, err = DecodeInvoice(truncated)
require.Error(t, err)
}
// TestNewInvoiceFromRequest verifies the constructor mirrors all non-signature
// invoice_request fields into the invoice, applies the invreq_amount ->
// invoice_amount writer rule, and does not copy the request's signature.
func TestNewInvoiceFromRequest(t *testing.T) {
t.Parallel()
_, bobPub := bobKey()
_, alicePub := aliceKey()
req := &InvoiceRequest{
OfferDescription: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](
tlv.Blob("description"),
),
),
OfferIssuerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType22](alicePub),
),
InvreqMetadata: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType0](
tlv.Blob("payer-metadata"),
),
),
InvreqPayerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType88](bobPub),
),
InvreqAmount: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType82, TUint64](2500),
),
Signature: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType240]([64]byte{0x01}),
),
}
inv := NewInvoiceFromRequest(req)
require.NotNil(t, inv)
// Non-signature request fields are mirrored exactly.
require.Equal(t, req.OfferDescription, inv.OfferDescription)
require.Equal(t, req.OfferIssuerID, inv.OfferIssuerID)
require.Equal(t, req.InvreqMetadata, inv.InvreqMetadata)
require.Equal(t, req.InvreqPayerID, inv.InvreqPayerID)
require.Equal(t, req.InvreqAmount, inv.InvreqAmount)
// invreq_amount is mirrored into invoice_amount per the writer rule.
require.Equal(t, TUint64(2500), inv.InvoiceAmount.UnwrapOrFailV(t))
// The request's signature is not copied. The invoice signs its own.
require.True(t, inv.Signature.IsNone())
}
// TestNewInvoiceFromRequestMirrorsUnknownFields verifies the writer requirement
// "MUST copy all non-signature fields from the invoice request (including
// unknown fields)": an unknown odd TLV in the request's signed range must
// survive into the constructed invoice's canonical record set so it is signed.
func TestNewInvoiceFromRequestMirrorsUnknownFields(t *testing.T) {
t.Parallel()
_, bobPub := bobKey()
// Build a minimal valid spontaneous request and encode it.
req := &InvoiceRequest{
OfferDescription: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](tlv.Blob("desc")),
),
InvreqMetadata: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType0](tlv.Blob("meta")),
),
InvreqPayerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType88](bobPub),
),
InvreqAmount: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType82, TUint64](1000),
),
}
encoded, err := req.Encode()
require.NoError(t, err)
// Fill in an unknown odd TLV (type 93, within the invreq signed range
// and above the request's existing types) so the spliced stream stays
// canonically sorted and the unknown lands in the decoded request's
// decodedTLVs sidecar.
const unknownType = 93
unknownVal := []byte("xyz")
var extra bytes.Buffer
require.NoError(t, tlv.WriteVarInt(&extra, unknownType, &[8]byte{}))
require.NoError(t, tlv.WriteVarInt(
&extra, uint64(len(unknownVal)), &[8]byte{},
))
extra.Write(unknownVal)
spliced := append(append([]byte{}, encoded...), extra.Bytes()...)
decodedReq, err := DecodeInvoiceRequest(spliced)
require.NoError(t, err)
inv := NewInvoiceFromRequest(decodedReq)
// The unknown field must appear in the invoice's canonical record set
// with its value preserved, not just its type.
var (
found bool
gotVal bytes.Buffer
)
for _, r := range inv.AllRecords() {
if r.Type() != unknownType {
continue
}
found = true
require.NoError(t, r.Encode(&gotVal))
}
require.True(t, found, "unknown request TLV not mirrored into invoice")
require.Equal(
t, unknownVal, gotVal.Bytes(),
"unknown request TLV value not preserved",
)
}
// TestInvoiceEncodeValidationGate verifies that Encode runs
// ValidateInvoiceWrite and rejects invalid invoices.
func TestInvoiceEncodeValidationGate(t *testing.T) {
t.Parallel()
inv := validInvoice(t)
inv.InvoiceCreatedAt = tlv.OptionalRecordT[
tlv.TlvType164, TUint64,
]{}
_, err := inv.Encode()
require.ErrorIs(t, err, ErrMissingCreatedAt)
}

167
bolt12/offer.go Normal file
View file

@ -0,0 +1,167 @@
package bolt12
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// Offer represents a BOLT 12 offer message. An offer is a long-lived, reusable
// payment template that can generate multiple invoices.
type Offer struct {
// OfferChains specifies which chains this offer is valid for. If
// absent, bitcoin is implied.
OfferChains tlv.OptionalRecordT[tlv.TlvType2, ChainsRecord]
// OfferMetadata is opaque data set by the offer creator for its own
// use.
OfferMetadata tlv.OptionalRecordT[tlv.TlvType4, tlv.Blob]
// OfferCurrency is the ISO 4217 currency code for the offer amount, if
// the amount is not in the chain's native unit.
OfferCurrency tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob]
// OfferAmount is the amount expected per item, encoded as a tu64. The
// unit depends on OfferCurrency (msat if absent).
OfferAmount tlv.OptionalRecordT[tlv.TlvType8, TUint64]
// OfferDescription is a UTF-8 description of the purpose of the
// payment.
OfferDescription tlv.OptionalRecordT[tlv.TlvType10, tlv.Blob]
// OfferFeatures is the feature bit vector for this offer.
OfferFeatures tlv.OptionalRecordT[tlv.TlvType12,
lnwire.RawFeatureVector]
// OfferAbsoluteExpiry is the time (seconds since epoch) after which the
// offer should not be used, encoded as a tu64.
OfferAbsoluteExpiry tlv.OptionalRecordT[tlv.TlvType14, TUint64]
// OfferPaths contains one or more blinded paths to the offer issuer.
OfferPaths tlv.OptionalRecordT[tlv.TlvType16, lnwire.BlindedPaths]
// OfferIssuer is a UTF-8 string identifying the issuer.
OfferIssuer tlv.OptionalRecordT[tlv.TlvType18, tlv.Blob]
// OfferQuantityMax is the maximum number of items that can be requested
// in a single invoice, encoded as a tu64. A value of 0 means unlimited.
OfferQuantityMax tlv.OptionalRecordT[tlv.TlvType20, TUint64]
// OfferIssuerID is the public key of the offer issuer. The codec
// parses the 33-byte SEC1 compressed point on decode, so a struct
// holding a key has already passed both the length and on-curve
// checks.
OfferIssuerID tlv.OptionalRecordT[tlv.TlvType22, *btcec.PublicKey]
// decodedTLVs is the canonical TypeMap produced by decoding this offer.
// Handled types map to nil; unhandled types map to their value bytes.
// Encoding and validation both derive their view from this single field
// so they cannot drift apart, and so signed-range extras the decoder
// did not understand are re-emitted on encode and preserve offer_id.
decodedTLVs tlv.TypeMap
}
var _ lnwire.PureTLVMessage = (*Offer)(nil)
// AllRecords returns the canonical sorted record list for this offer, merging
// the typed records with any extra signed-range fields that the decoder
// preserved.
func (o *Offer) AllRecords() []tlv.Record {
return allRecordsFromTypeMap(
o.allRecordProducers(), o.decodedTLVs,
)
}
// allRecordProducers returns record producers for every set optional field, in
// declaration order.
func (o *Offer) allRecordProducers() []tlv.RecordProducer {
var p []tlv.RecordProducer
lnwire.AddOpt(&p, o.OfferChains)
lnwire.AddOpt(&p, o.OfferMetadata)
lnwire.AddOpt(&p, o.OfferCurrency)
lnwire.AddOpt(&p, o.OfferAmount)
lnwire.AddOpt(&p, o.OfferDescription)
lnwire.AddOpt(&p, o.OfferFeatures)
lnwire.AddOpt(&p, o.OfferAbsoluteExpiry)
lnwire.AddOpt(&p, o.OfferPaths)
lnwire.AddOpt(&p, o.OfferIssuer)
lnwire.AddOpt(&p, o.OfferQuantityMax)
lnwire.AddOpt(&p, o.OfferIssuerID)
return p
}
// Encode serialises the offer into a canonical TLV byte stream.
func (o *Offer) Encode() ([]byte, error) {
if err := ValidateOfferWrite(o); err != nil {
return nil, fmt.Errorf("validate offer: %w", err)
}
var buf bytes.Buffer
if err := lnwire.EncodePureTLVMessage(o, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// decodeOffer parses a TLV byte stream into an Offer. Decoding is permissive —
// the spec writer requirements are not enforced here, so callers that need a
// valid offer must run ValidateOfferRead. Unknown TLVs are preserved on the
// returned offer so a later Encode can re-emit signed-range extras and keep
// offer_id stable.
func decodeOffer(data []byte) (*Offer, error) {
var o Offer
// Prepare zero-valued records for all optional fields so the TLV
// decoder can populate them.
chains := tlv.ZeroRecordT[tlv.TlvType2, ChainsRecord]()
metadata := tlv.ZeroRecordT[tlv.TlvType4, tlv.Blob]()
currency := tlv.ZeroRecordT[tlv.TlvType6, tlv.Blob]()
amount := tlv.ZeroRecordT[tlv.TlvType8, TUint64]()
desc := tlv.ZeroRecordT[tlv.TlvType10, tlv.Blob]()
features := tlv.ZeroRecordT[tlv.TlvType12, lnwire.RawFeatureVector]()
expiry := tlv.ZeroRecordT[tlv.TlvType14, TUint64]()
paths := tlv.ZeroRecordT[tlv.TlvType16, lnwire.BlindedPaths]()
issuer := tlv.ZeroRecordT[tlv.TlvType18, tlv.Blob]()
qtyMax := tlv.ZeroRecordT[tlv.TlvType20, TUint64]()
issuerID := tlv.ZeroRecordT[tlv.TlvType22, *btcec.PublicKey]()
tm, err := decodeStream(
data,
chains.Record(),
metadata.Record(),
currency.Record(),
amount.Record(),
desc.Record(),
features.Record(),
expiry.Record(),
paths.Record(),
issuer.Record(),
qtyMax.Record(),
issuerID.Record(),
)
if err != nil {
return nil, fmt.Errorf("decode offer: %w", err)
}
lnwire.SetOptFromMap(tm, &o.OfferChains, chains)
lnwire.SetOptFromMap(tm, &o.OfferMetadata, metadata)
lnwire.SetOptFromMap(tm, &o.OfferCurrency, currency)
lnwire.SetOptFromMap(tm, &o.OfferAmount, amount)
lnwire.SetOptFromMap(tm, &o.OfferDescription, desc)
lnwire.SetOptFromMap(tm, &o.OfferFeatures, features)
lnwire.SetOptFromMap(tm, &o.OfferAbsoluteExpiry, expiry)
lnwire.SetOptFromMap(tm, &o.OfferPaths, paths)
lnwire.SetOptFromMap(tm, &o.OfferIssuer, issuer)
lnwire.SetOptFromMap(tm, &o.OfferQuantityMax, qtyMax)
lnwire.SetOptFromMap(tm, &o.OfferIssuerID, issuerID)
o.decodedTLVs = tm
return &o, nil
}

49
bolt12/offer_test.go Normal file
View file

@ -0,0 +1,49 @@
package bolt12
import (
"testing"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
// TestOfferRoundTrip pins encode→decode→re-encode for an Offer with a
// representative subset of optional fields. A byte-identical re-encode is the
// invariant that keeps offer_id stable across the codec boundary.
func TestOfferRoundTrip(t *testing.T) {
t.Parallel()
desc := tlv.Blob("coffee")
issuer := tlv.Blob("alice")
_, bobPub := bobKey()
o := &Offer{
OfferAmount: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType8](TUint64(1500)),
),
OfferDescription: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType10](desc),
),
OfferIssuer: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType18](issuer),
),
OfferIssuerID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType22](bobPub),
),
}
encoded, err := o.Encode()
require.NoError(t, err)
require.NotEmpty(t, encoded)
decoded, err := decodeOffer(encoded)
require.NoError(t, err)
require.Equal(t, TUint64(1500), decoded.OfferAmount.UnwrapOrFailV(t))
require.Equal(t, desc, decoded.OfferDescription.UnwrapOrFailV(t))
require.Equal(t, issuer, decoded.OfferIssuer.UnwrapOrFailV(t))
reencoded, err := decoded.Encode()
require.NoError(t, err)
require.Equal(t, encoded, reencoded)
}

52
bolt12/pure_tlv.go Normal file
View file

@ -0,0 +1,52 @@
package bolt12
import (
"slices"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// bolt12InUnsignedRange reports whether a TLV type is excluded from the BOLT 12
// Merkle tree. The spec reserves types 240-1000 for signature TLVs (the BIP-340
// Schnorr signatures over the tree itself); every other allowed type sits in
// the signed range.
func bolt12InUnsignedRange(t tlv.Type) bool {
return t >= 240 && t <= 1000
}
// allRecordsFromTypeMap merges the typed-record producers with the signed-range
// subset of the supplied TypeMap (preserved unknown TLVs) and returns the
// canonical sorted record list. The signed-range subset is derived on demand
// from the same TypeMap that drives the validators, so the two views cannot
// drift apart.
func allRecordsFromTypeMap(producers []tlv.RecordProducer,
tm tlv.TypeMap) []tlv.Record {
if len(tm) > 0 {
extra := lnwire.ExtraSignedFieldsFromTypeMapFn(
tm, bolt12InUnsignedRange,
)
if len(extra) > 0 {
producers = append(
producers, lnwire.RecordsAsProducers(
tlv.MapToRecords(extra),
)...,
)
}
}
return lnwire.ProduceRecordsSorted(producers...)
}
// sortedTypes returns the keys of tm in ascending order. Validators iterate the
// result for deterministic out-of-range and unknown-even error messages.
func sortedTypes(tm tlv.TypeMap) []tlv.Type {
out := make([]tlv.Type, 0, len(tm))
for t := range tm {
out = append(out, t)
}
slices.Sort(out)
return out
}

448
bolt12/subtypes.go Normal file
View file

@ -0,0 +1,448 @@
package bolt12
import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// ErrTooManyChains is returned when offer_chains declares more entries than
// maxOfferChains.
var ErrTooManyChains = errors.New("offer_chains exceeds maxOfferChains")
// ErrNonMinimalFeatures is returned when a decoded feature vector is not
// canonically (minimally) encoded.
var ErrNonMinimalFeatures = errors.New("non-minimal feature vector encoding")
// ErrTooManyBlindedPayInfos is returned when decoded blinded_payinfo entries
// exceed maxBlindedPayInfos.
var ErrTooManyBlindedPayInfos = errors.New(
"invoice_blindedpay exceeds maxBlindedPayInfos",
)
// ErrInvalidHtlcRange is returned when a decoded blinded_payinfo entry carries
// an htlc_minimum_msat greater than its htlc_maximum_msat.
var ErrInvalidHtlcRange = errors.New(
"blinded_payinfo htlc_minimum_msat exceeds htlc_maximum_msat",
)
// ErrTooManyFallbackAddrs is returned when decoded fallback_address entries
// exceed maxFallbackAddrs.
var ErrTooManyFallbackAddrs = errors.New(
"invoice_fallbacks exceeds maxFallbackAddrs",
)
const (
// chainHashLen is the length of a chain hash (32 bytes).
chainHashLen = 32
// maxOfferChains caps decoded offer_chains entries. This is a sanity
// check to prevent excessive memory allocation and is not a protocol
// limit but a local implementation choice.
maxOfferChains = 32
// maxBlindedPayInfos caps decoded blinded_payinfo entries to prevent
// excessive allocation and validation cost.
maxBlindedPayInfos = 32
// maxFallbackAddrs caps decoded fallback_address entries to prevent
// excessive allocation and validation cost.
maxFallbackAddrs = 32
// maxFallbackAddrLen bounds the address bytes in a single fallback
// entry. The spec encodes the length as a uint16, so 65535 is the
// format's ceiling.
maxFallbackAddrLen = math.MaxUint16
)
// ChainsRecord holds one or more chain hashes for the offer_chains field.
type ChainsRecord struct {
Chains [][chainHashLen]byte
}
var _ tlv.RecordProducer = (*ChainsRecord)(nil)
// Record returns a TLV record for ChainsRecord.
func (c *ChainsRecord) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, c,
func() uint64 {
return uint64(len(c.Chains)) * chainHashLen
},
encodeChainsRecord,
decodeChainsRecord,
)
}
// encodeChainsRecord writes the chain hashes in sequence, without a count
// prefix.
func encodeChainsRecord(w io.Writer, val any, _ *[8]byte) error {
c, ok := val.(*ChainsRecord)
if !ok {
return fmt.Errorf("expected *ChainsRecord, got %T", val)
}
for _, chain := range c.Chains {
if _, err := w.Write(chain[:]); err != nil {
return err
}
}
return nil
}
// decodeChainsRecord caps the count at maxOfferChains to bound allocation.
func decodeChainsRecord(r io.Reader, val any, _ *[8]byte, l uint64) error {
c, ok := val.(*ChainsRecord)
if !ok {
return fmt.Errorf("expected *ChainsRecord, got %T", val)
}
if l%chainHashLen != 0 {
return fmt.Errorf("chains length %d not a multiple of %d", l,
chainHashLen)
}
numChains := l / chainHashLen
if numChains > maxOfferChains {
return fmt.Errorf("%w: %d > %d", ErrTooManyChains, numChains,
maxOfferChains)
}
c.Chains = make([][chainHashLen]byte, numChains)
for i := range c.Chains {
if _, err := io.ReadFull(r, c.Chains[i][:]); err != nil {
return err
}
}
return nil
}
// BlindedPayInfo holds the payment parameters for a blinded path, corresponding
// to the blinded_payinfo subtype.
type BlindedPayInfo struct {
// FeeBaseMsat is the base fee, in millisatoshis, charged for relaying a
// payment over this blinded path.
FeeBaseMsat uint32
// FeeProportionalMillionths is the proportional fee, in millionths of a
// satoshi per relayed satoshi, charged over this blinded path.
FeeProportionalMillionths uint32
// CltvExpiryDelta is the CLTV expiry delta the path requires.
CltvExpiryDelta uint16
// HtlcMinimumMsat is the smallest HTLC, in millisatoshis, the path
// accepts.
HtlcMinimumMsat uint64
// HtlcMaximumMsat is the largest HTLC, in millisatoshis, the path
// accepts.
HtlcMaximumMsat uint64
// Features is the relay feature bitmap for this blinded path, typed for
// consistency with the other BOLT 12 feature fields.
//
// WARNING: RawFeatureVector re-encodes to minimal length, so setting
// non-minimal feature bytes (trailing zeros) yields different wire
// bytes than were read and invalidates the invoice signature.
Features lnwire.RawFeatureVector
}
// BlindedPayInfos holds a list of BlindedPayInfo entries for the
// invoice_blindedpay field.
type BlindedPayInfos struct {
Infos []BlindedPayInfo
}
// Record returns a TLV record for BlindedPayInfos.
//
// NOTE: This implements the tlv.RecordProducer interface.
func (bp *BlindedPayInfos) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, bp,
func() uint64 {
return blindedPayInfosSize(bp)
},
encodeBlindedPayInfos, decodeBlindedPayInfos,
)
}
// blindedPayInfosSize returns the encoded byte length of all blinded_payinfo
// entries, used to size the dynamic TLV record.
func blindedPayInfosSize(bp *BlindedPayInfos) uint64 {
var size uint64
for _, info := range bp.Infos {
// fee_base(4) + fee_prop(4) + cltv(2) + htlc_min(8) +
// htlc_max(8) + flen(2) + features.
size += 4 + 4 + 2 + 8 + 8 + 2 +
uint64(info.Features.SerializeSize())
}
return size
}
// encodeBlindedPayInfos writes each blinded_payinfo entry in sequence: the
// fixed fee, cltv and htlc fields followed by a u16-length-prefixed feature
// vector. Entries are concatenated without a count prefix; the count is
// recovered on decode from the surrounding invoice_paths length.
func encodeBlindedPayInfos(
w io.Writer, val interface{}, buf *[8]byte) error {
bp, ok := val.(*BlindedPayInfos)
if !ok {
return fmt.Errorf("expected *BlindedPayInfos, got %T", val)
}
for _, info := range bp.Infos {
binary.BigEndian.PutUint32(buf[:4], info.FeeBaseMsat)
if _, err := w.Write(buf[:4]); err != nil {
return err
}
binary.BigEndian.PutUint32(
buf[:4], info.FeeProportionalMillionths,
)
if _, err := w.Write(buf[:4]); err != nil {
return err
}
binary.BigEndian.PutUint16(buf[:2], info.CltvExpiryDelta)
if _, err := w.Write(buf[:2]); err != nil {
return err
}
binary.BigEndian.PutUint64(buf[:8], info.HtlcMinimumMsat)
if _, err := w.Write(buf[:8]); err != nil {
return err
}
binary.BigEndian.PutUint64(buf[:8], info.HtlcMaximumMsat)
if _, err := w.Write(buf[:8]); err != nil {
return err
}
// flen is a u16, so guard the cast before framing the minimal
// feature bytes, mirroring encodeFallbackAddrs.
flen := info.Features.SerializeSize()
if flen > math.MaxUint16 {
return fmt.Errorf("features %d exceed limit %d",
flen, math.MaxUint16)
}
binary.BigEndian.PutUint16(buf[:2], uint16(flen))
if _, err := w.Write(buf[:2]); err != nil {
return err
}
if err := info.Features.EncodeBase256(w); err != nil {
return err
}
}
return nil
}
// decodeBlindedPayInfos reads blinded_payinfo entries until the record bytes
// are exhausted. The entry count is capped at maxBlindedPayInfos to prevent
// excessive memory allocation and validation cost.
func decodeBlindedPayInfos(
r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
bp, ok := val.(*BlindedPayInfos)
if !ok {
return fmt.Errorf("expected *BlindedPayInfos, got %T", val)
}
lr := &io.LimitedReader{R: r, N: int64(l)}
for lr.N > 0 {
if len(bp.Infos) >= maxBlindedPayInfos {
return ErrTooManyBlindedPayInfos
}
var info BlindedPayInfo
if _, err := io.ReadFull(lr, buf[:4]); err != nil {
return fmt.Errorf("read fee_base: %w", err)
}
info.FeeBaseMsat = binary.BigEndian.Uint32(buf[:4])
if _, err := io.ReadFull(lr, buf[:4]); err != nil {
return fmt.Errorf("read fee_prop: %w", err)
}
info.FeeProportionalMillionths = binary.BigEndian.Uint32(
buf[:4],
)
if _, err := io.ReadFull(lr, buf[:2]); err != nil {
return fmt.Errorf("read cltv_delta: %w", err)
}
info.CltvExpiryDelta = binary.BigEndian.Uint16(buf[:2])
if _, err := io.ReadFull(lr, buf[:8]); err != nil {
return fmt.Errorf("read htlc_min: %w", err)
}
info.HtlcMinimumMsat = binary.BigEndian.Uint64(buf[:8])
if _, err := io.ReadFull(lr, buf[:8]); err != nil {
return fmt.Errorf("read htlc_max: %w", err)
}
info.HtlcMaximumMsat = binary.BigEndian.Uint64(buf[:8])
// Defense-in-depth decode check, mirroring the
// ErrNonMinimalFeatures guard below: reject an inverted HTLC
// range so the htlc_min <= htlc_max invariant holds for every
// downstream consumer instead of being re-derived per caller.
if info.HtlcMinimumMsat > info.HtlcMaximumMsat {
return ErrInvalidHtlcRange
}
// flen then features, mirroring decodeFallbackAddrs: reject a
// length that overruns the remaining bytes before allocating.
// Decode into a constructed vector so its map is initialised.
if _, err := io.ReadFull(lr, buf[:2]); err != nil {
return fmt.Errorf("read flen: %w", err)
}
flen := binary.BigEndian.Uint16(buf[:2])
if int64(flen) > lr.N {
return fmt.Errorf("flen %d exceeds remaining %d",
flen, lr.N)
}
fv := lnwire.NewRawFeatureVector()
if err := fv.DecodeBase256(lr, int(flen)); err != nil {
return fmt.Errorf("read features: %w", err)
}
if fv.SerializeSize() != int(flen) {
return ErrNonMinimalFeatures
}
info.Features = *fv
bp.Infos = append(bp.Infos, info)
}
return nil
}
// FallbackAddress represents an on-chain fallback address.
type FallbackAddress struct {
Version byte
Address []byte
}
// FallbackAddresses holds a list of fallback addresses for the
// invoice_fallbacks field.
type FallbackAddresses struct {
Addrs []FallbackAddress
}
// Record returns a TLV record for FallbackAddresses.
//
// NOTE: This implements the tlv.RecordProducer interface.
func (fa *FallbackAddresses) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, fa,
func() uint64 {
return fallbackAddrsSize(fa)
},
encodeFallbackAddrs, decodeFallbackAddrs,
)
}
// fallbackAddrsSize returns the encoded byte length of all fallback_address
// entries, used to size the dynamic TLV record.
func fallbackAddrsSize(fa *FallbackAddresses) uint64 {
var size uint64
for _, a := range fa.Addrs {
// version(1) + len(2) + address
size += 1 + 2 + uint64(len(a.Address))
}
return size
}
// encodeFallbackAddrs writes each fallback_address entry as a version byte, a
// u16 address length and the raw address bytes, concatenated without a count
// prefix.
func encodeFallbackAddrs(
w io.Writer, val interface{}, buf *[8]byte) error {
fa, ok := val.(*FallbackAddresses)
if !ok {
return fmt.Errorf("expected *FallbackAddresses, got %T", val)
}
for i, a := range fa.Addrs {
if len(a.Address) > maxFallbackAddrLen {
return fmt.Errorf("fallback %d: address %d exceeds "+
"limit %d", i, len(a.Address),
maxFallbackAddrLen)
}
buf[0] = a.Version
if _, err := w.Write(buf[:1]); err != nil {
return err
}
binary.BigEndian.PutUint16(buf[:2], uint16(len(a.Address)))
if _, err := w.Write(buf[:2]); err != nil {
return err
}
if _, err := w.Write(a.Address); err != nil {
return err
}
}
return nil
}
// decodeFallbackAddrs reads fallback_address entries until the record bytes are
// exhausted. The entry count is capped at maxFallbackAddrs to prevent
// excessive memory allocation and validation cost.
func decodeFallbackAddrs(
r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
fa, ok := val.(*FallbackAddresses)
if !ok {
return fmt.Errorf("expected *FallbackAddresses, got %T", val)
}
lr := &io.LimitedReader{R: r, N: int64(l)}
for lr.N > 0 {
if len(fa.Addrs) >= maxFallbackAddrs {
return ErrTooManyFallbackAddrs
}
var a FallbackAddress
if _, err := io.ReadFull(lr, buf[:1]); err != nil {
return fmt.Errorf("read version: %w", err)
}
a.Version = buf[0]
if _, err := io.ReadFull(lr, buf[:2]); err != nil {
return fmt.Errorf("read addrlen: %w", err)
}
addrLen := binary.BigEndian.Uint16(buf[:2])
if int64(addrLen) > lr.N {
return fmt.Errorf("addrlen %d exceeds remaining %d",
addrLen, lr.N)
}
a.Address = make([]byte, addrLen)
if _, err := io.ReadFull(lr, a.Address); err != nil {
return fmt.Errorf("read address: %w", err)
}
fa.Addrs = append(fa.Addrs, a)
}
return nil
}

415
bolt12/subtypes_test.go Normal file
View file

@ -0,0 +1,415 @@
package bolt12
import (
"bytes"
"encoding/hex"
"math"
"testing"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestDecodeChainsRecord pins the chain-array decoder's structural rejections.
func TestDecodeChainsRecord(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
wantErr error
wantMsg string
}{
{
name: "length not multiple of 32",
data: append(
bytes.Repeat(
[]byte{0xaa}, chainHashLen,
),
187,
),
wantMsg: "not a multiple of",
},
{
name: "exceeds cap",
data: bytes.Repeat(
[]byte{0x00}, (maxOfferChains+1)*chainHashLen,
),
wantErr: ErrTooManyChains,
},
}
for _, tc := range tests {
t.Run(
tc.name,
func(t *testing.T) {
t.Parallel()
var c ChainsRecord
err := decodeChainsRecord(
bytes.NewReader(tc.data), &c,
new([8]byte),
uint64(
len(tc.data),
),
)
require.Error(t, err)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
}
if tc.wantMsg != "" {
require.Contains(
t, err.Error(), tc.wantMsg,
)
}
},
)
}
}
// TestChainsRecordRoundTrip pins decode→re-encode against the BOLT 12 offer
// test vectors.
func TestChainsRecordRoundTrip(t *testing.T) {
t.Parallel()
// bitcoinHash is the bitcoin mainnet genesis hash hex-decoded into a
// fixed array. Defined locally so the test does not depend on constants
// introduced by later commits.
bitcoinHashHex := "6fe28c0ab6f1b372c1a6a246ae63f74f931e8365" +
"e15a089c68d6190000000000"
var bitcoinHash [chainHashLen]byte
bitcoinHashBytes, err := hex.DecodeString(bitcoinHashHex)
require.NoError(t, err)
copy(bitcoinHash[:], bitcoinHashBytes)
tests := []struct {
name string
// hex is the on-wire bytes of the offer_chains TLV value
// (concatenated 32-byte chain hashes), copied from
// bolt12/offers-test.json.
hex string
wantLen int
wantHash [chainHashLen]byte
}{
{
name: "single testnet chain",
hex: "43497fd7f826957108f4a30fd9cec3ae" +
"ba79972084e90ead01ea330900000000",
wantLen: 1,
},
{
name: "single bitcoin chain",
hex: bitcoinHashHex,
wantLen: 1,
wantHash: bitcoinHash,
},
{
name: "two chains liquidv1 then bitcoin",
hex: "1466275836220db2944ca059a3a10ef6fd2ea684b" +
"0688d2c379296888a206003" + bitcoinHashHex,
wantLen: 2,
// Second chain in the list is bitcoin mainnet.
wantHash: bitcoinHash,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data, err := hex.DecodeString(tc.hex)
require.NoError(t, err)
var c ChainsRecord
err = decodeChainsRecord(
bytes.NewReader(data), &c, new([8]byte),
uint64(
len(data),
),
)
require.NoError(t, err)
require.Len(t, c.Chains, tc.wantLen)
// Cross-check the canonical bitcoin chain hash where
// the row knows which slot it lives in.
var zero [chainHashLen]byte
if tc.wantHash != zero {
idx := tc.wantLen - 1
require.Equal(
t, tc.wantHash, c.Chains[idx],
"bitcoin hash mismatch in slot %d",
idx,
)
}
var buf bytes.Buffer
require.NoError(
t, encodeChainsRecord(&buf, &c, new([8]byte)),
)
require.Equal(t, data, buf.Bytes())
})
}
}
// TestFallbackAddressesRoundTrip encodes a list of fallback addresses
// covering BIP-141 v0, BIP-350 v1, a forward-compatible v2 entry, and
// a v17 entry that the spec mandates a *reader* ignore but the codec
// layer must still round-trip faithfully (the ignore policy lives at
// the invoice-consumer layer, not at the codec). The fallback list is
// on-chain payment data: a wrong version byte or mis-framed length
// translates into funds going to an unintended script, so encode/
// decode must be a faithful bijection across the entire version
// range.
func TestFallbackAddressesRoundTrip(t *testing.T) {
t.Parallel()
addrs := &FallbackAddresses{
Addrs: []FallbackAddress{
{
Version: 0,
Address: bytes.Repeat([]byte{0xab}, 20),
},
{
Version: 1,
Address: bytes.Repeat([]byte{0xcd}, 32),
},
{
Version: 2,
Address: bytes.Repeat([]byte{0xef}, 64),
},
{
Version: 17,
Address: bytes.Repeat([]byte{0x99}, 20),
},
},
}
var buf bytes.Buffer
require.NoError(t, encodeFallbackAddrs(&buf, addrs, new([8]byte)))
encoded := buf.Bytes()
expectedSize := fallbackAddrsSize(addrs)
require.Equal(t, expectedSize, uint64(len(encoded)))
var decoded FallbackAddresses
err := decodeFallbackAddrs(
bytes.NewReader(encoded), &decoded, new([8]byte),
uint64(len(encoded)),
)
require.NoError(t, err)
require.Equal(t, addrs.Addrs, decoded.Addrs)
}
// TestBlindedPayInfosRoundTrip encodes a list of blinded_payinfo entries and
// asserts decode reproduces them exactly.
func TestBlindedPayInfosRoundTrip(t *testing.T) {
t.Parallel()
noFeats := *lnwire.NewRawFeatureVector()
someFeats := *lnwire.NewRawFeatureVector(8, 15)
infos := &BlindedPayInfos{
Infos: []BlindedPayInfo{
{
FeeBaseMsat: 1000,
FeeProportionalMillionths: 250,
CltvExpiryDelta: 144,
HtlcMinimumMsat: 1,
HtlcMaximumMsat: 1_000_000,
Features: noFeats,
},
{
FeeBaseMsat: 0,
FeeProportionalMillionths: 0,
CltvExpiryDelta: 40,
HtlcMinimumMsat: 0,
HtlcMaximumMsat: math.MaxUint64,
Features: someFeats,
},
},
}
var buf bytes.Buffer
require.NoError(t, encodeBlindedPayInfos(&buf, infos, new([8]byte)))
encoded := buf.Bytes()
require.Equal(t, blindedPayInfosSize(infos), uint64(len(encoded)))
var decoded BlindedPayInfos
err := decodeBlindedPayInfos(
bytes.NewReader(encoded), &decoded,
new([8]byte), uint64(len(encoded)),
)
require.NoError(t, err)
require.Equal(t, infos.Infos, decoded.Infos)
}
// TestDecodeBlindedPayInfosRejectsTruncated covers truncation before the fixed
// fields and before the declared features payload. Each must fail rather than
// yield a partial BlindedPayInfos with corrupt entries.
func TestDecodeBlindedPayInfosRejectsTruncated(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
declLen uint64
errSubstr string
}{
{
name: "missing fee_base",
data: nil,
declLen: 4,
errSubstr: "read fee_base",
},
{
name: "features length exceeds remaining",
// fee_base(4) fee_prop(4) cltv(2) htlc_min(8)
// htlc_max(8) then flen=0xffff with no payload.
data: append(
make([]byte, 26), []byte{0xff, 0xff}...,
),
declLen: 28,
errSubstr: "exceeds remaining",
},
{
name: "exceeds cap",
data: make([]byte, (maxBlindedPayInfos+1)*28),
declLen: (maxBlindedPayInfos + 1) * 28,
errSubstr: "exceeds maxBlindedPayInfos",
},
{
name: "non-minimal features",
// fee_base(4) + fee_prop(4) + cltv(2) + htlc_min(8) +
// htlc_max(8) followed by flen = 1, and 1 non-minimal
// feature byte (trailing zero).
data: append(
make([]byte, 26), []byte{0x00, 0x01, 0x00}...,
),
declLen: 29,
errSubstr: "non-minimal",
},
{
name: "inverted htlc range",
// htlc_min at bytes [10:18] = 1000, htlc_max at bytes
// [18:26] = 500, so min > max must be rejected before
// the flen/features are ever read.
data: func() []byte {
b := make([]byte, 26)
b[16], b[17] = 0x03, 0xe8 // htlc_min = 1000
b[24], b[25] = 0x01, 0xf4 // htlc_max = 500
return b
}(),
declLen: 26,
errSubstr: "htlc_minimum_msat exceeds",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var bp BlindedPayInfos
err := decodeBlindedPayInfos(
bytes.NewReader(tc.data), &bp, new([8]byte),
tc.declLen,
)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errSubstr)
})
}
}
// TestEncodeFallbackAddrsRejectsOversize asserts the maxFallbackAddrLen cap is
// enforced before any bytes hit the writer.
func TestEncodeFallbackAddrsRejectsOversize(t *testing.T) {
t.Parallel()
addrs := &FallbackAddresses{
Addrs: []FallbackAddress{{
Version: 0,
Address: make([]byte, maxFallbackAddrLen+1),
}},
}
var buf bytes.Buffer
err := encodeFallbackAddrs(&buf, addrs, new([8]byte))
require.Error(t, err)
require.Contains(t, err.Error(), "exceeds limit")
require.Zero(t, buf.Len(),
"no bytes should be written when validation fails")
}
// TestDecodeFallbackAddrsRejectsTruncated covers the three truncation points in
// decodeFallbackAddrs: stream ends before the version byte, before the 16-bit
// length, and before the address payload of the declared size. Each must fail
// with an error rather than yielding a partial FallbackAddresses with corrupt
// entries.
func TestDecodeFallbackAddrsRejectsTruncated(t *testing.T) {
t.Parallel()
// Each case declares a TLV-record length that overshoots the bytes
// actually present, simulating a malformed wire payload that promises
// more data than it delivers.
tests := []struct {
name string
data []byte
declLen uint64
errSubstr string
}{
{
name: "missing version byte",
data: nil,
declLen: 1,
errSubstr: "read version",
},
{
name: "missing length bytes",
data: []byte{0x00},
declLen: 3,
errSubstr: "read addrlen",
},
{
name: "truncated address payload",
data: []byte{
0x00, 0x00, 0x05, 0xab, 0xab,
},
declLen: 8,
errSubstr: "read address",
},
{
// addrlen > remaining trips the guard before
// allocation; without it a hostile addrlen would force
// a huge make([]byte, addrLen).
name: "addrlen exceeds remaining",
data: []byte{0x00, 0xff, 0xff, 0xab},
declLen: 4,
errSubstr: "exceeds remaining",
},
{
name: "exceeds cap",
data: make([]byte, (maxFallbackAddrs+1)*3),
declLen: (maxFallbackAddrs + 1) * 3,
errSubstr: "exceeds maxFallbackAddrs",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var fa FallbackAddresses
err := decodeFallbackAddrs(
bytes.NewReader(tc.data), &fa, new([8]byte),
tc.declLen,
)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errSubstr)
})
}
}

39
bolt12/tlv_types.go Normal file
View file

@ -0,0 +1,39 @@
package bolt12
import (
"github.com/lightningnetwork/lnd/tlv"
)
// TUint64 is a uint64 that serializes using truncated encoding (tu64)
// as required by BOLT 12. Leading zero bytes are omitted.
type TUint64 uint64
// Record returns a TLV record using truncated uint64 encoding.
//
// NOTE: This implements the tlv.RecordProducer interface.
func (t *TUint64) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, (*uint64)(t),
func() uint64 {
return tlv.SizeTUint64(uint64(*t))
},
tlv.ETUint64, tlv.DTUint64,
)
}
// TUint32 is a uint32 that serializes using truncated encoding (tu32) as
// required by BOLT 12. Leading zero bytes are omitted.
type TUint32 uint32
// Record returns a TLV record using truncated uint32 encoding.
//
// NOTE: This implements the tlv.RecordProducer interface.
func (t *TUint32) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, (*uint32)(t),
func() uint64 {
return tlv.SizeTUint32(uint32(*t))
},
tlv.ETUint32, tlv.DTUint32,
)
}

1845
bolt12/validate.go Normal file

File diff suppressed because it is too large Load diff

2776
bolt12/validate_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -92,7 +92,6 @@ func TestParseAndSetDebugLevels(t *testing.T) {
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
m := &mockSubLogger{
subLogLevels: make(map[string]string),

View file

@ -44,10 +44,10 @@ const (
AppMajor uint = 0
// AppMinor defines the minor version of this binary.
AppMinor uint = 20
AppMinor uint = 21
// AppPatch defines the application patch for this binary.
AppPatch uint = 00
AppPatch uint = 99
// AppPreRelease MUST only contain characters from semanticAlphabet per
// the semantic versioning spec.

View file

@ -1,6 +1,6 @@
module github.com/lightningnetwork/lnd/cert
go 1.19
go 1.25.11
require github.com/stretchr/testify v1.8.2

View file

@ -6,7 +6,7 @@ import (
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/wire/v2"
)
// BestBlockView is an interface that allows the querying of the most

View file

@ -7,8 +7,8 @@ import (
"testing/quick"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntest/wait"

View file

@ -1,17 +1,18 @@
package bitcoindnotify
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"
@ -175,7 +176,7 @@ func (b *BitcoindNotifier) startNotifier() error {
// Connect to bitcoind, and register for notifications on connected,
// and disconnected blocks.
if err := b.chainConn.Start(); err != nil {
if err := b.chainConn.Start(context.Background()); err != nil {
return err
}
if err := b.chainConn.NotifyBlocks(); err != nil {

View file

@ -4,10 +4,11 @@
package bitcoindnotify
import (
"context"
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/chainntnfs"
)
@ -23,7 +24,7 @@ func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Has
// Connect to bitcoind, and register for notifications on connected,
// and disconnected blocks.
if err := b.chainConn.Start(); err != nil {
if err := b.chainConn.Start(context.Background()); err != nil {
return err
}
if err := b.chainConn.NotifyBlocks(); err != nil {

View file

@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcwallet/chain"

View file

@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"

View file

@ -8,12 +8,12 @@ import (
"time"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/lightningnetwork/lnd/chainntnfs"
)

View file

@ -7,7 +7,7 @@ import (
"bytes"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"

View file

@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/rpcclient"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/chainntnfs"

View file

@ -10,9 +10,9 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/fn/v2"
)

View file

@ -3,7 +3,7 @@
package chainntnfs
import "github.com/btcsuite/btcd/chaincfg/chainhash"
import "github.com/btcsuite/btcd/chainhash/v2"
// TestChainNotifier enables the use of methods that are only present during
// testing for ChainNotifiers.

View file

@ -4,8 +4,8 @@ import (
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/lnutils"
)
@ -298,8 +298,6 @@ func (m *MempoolNotifier) notifySpent(spentInputs inputsWithTx) {
// Iterate the spent inputs to notify the subscribers concurrently.
for op, tx := range spentInputs {
op, tx := op, tx
m.wg.Add(1)
go notifyAll(tx, op)
}

View file

@ -4,8 +4,8 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/stretchr/testify/require"
)

View file

@ -1,8 +1,8 @@
package chainntnfs
import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/mock"
)

View file

@ -9,12 +9,12 @@ import (
"time"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/gcs/builder"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/btcutil/v2/gcs/builder"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/headerfs"
"github.com/lightningnetwork/lnd/blockcache"

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/rpcclient"
"github.com/lightninglabs/neutrino"
"github.com/lightningnetwork/lnd/chainntnfs"

View file

@ -11,11 +11,11 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btcwallet/chain"
_ "github.com/btcsuite/btcwallet/walletdb/bdb" // Required to auto-register the boltdb walletdb implementation.
"github.com/lightninglabs/neutrino"

View file

@ -9,13 +9,13 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/address/v2"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntest/unittest"
"github.com/stretchr/testify/require"
@ -36,8 +36,8 @@ func randPubKeyHashScript() ([]byte, *btcec.PrivateKey, error) {
return nil, nil, err
}
pubKeyHash := btcutil.Hash160(privKey.PubKey().SerializeCompressed())
addrScript, err := btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash := address.Hash160(privKey.PubKey().SerializeCompressed())
addrScript, err := address.NewAddressWitnessPubKeyHash(
pubKeyHash, unittest.NetParams,
)
if err != nil {

Some files were not shown because too many files have changed in this diff Show more