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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
When the switch forwards a blinded hop identified by node ID, it has not
yet resolved a concrete outgoing channel at interception time. Expose the
next hop to the interceptor: InterceptedForward.Packet() reports the
packet's outgoing channel as-is (hop.Exit, since none is selected yet) and
carries the requested pubkey in OutgoingNodeID.
At the RPC boundary, forwardInterceptor.onIntercept maps a node-ID hop to
the reserved NodeIDForwardSCID sentinel in outgoing_requested_chan_id and
the pubkey in outgoing_requested_node_id, so a client switching on a zero
channel ID to detect the exit hop does not misread the forward as a final
receive. The sentinel is a wire-only concern, applied where the request is
built rather than in the switch's internal InterceptedPacket, which stays
truthful (OutgoingNodeID.IsSome() is the node-ID discriminator).
Now that the switch forwards blinded hops identified by node ID, a new
problem surfaces in the HTLC event stream. A node-ID next hop has no
outgoing short channel ID until non-strict forwarding selects one, so a
forward that fails before selection still carries outgoingChanID ==
hop.Exit. getEventType keys the exit hop off that sentinel, so it
misclassifies such a failed node-ID forward as a receive, mislabeling the
event streamed via SubscribeHtlcEvents (a forwarding failure reported as
a receive failure).
Two paths reach getEventType before an SCID is selected: the fail packet
built by failAddPacket and the resolution packet built by resolve, both
of which dropped the decoded next hop. Carry outgoingHop into both, and
classify a Right (node-ID) outgoingHop as a forward before the hop.Exit
check. A node-ID next hop is always a forward, never the exit hop.
Fixeslightningnetwork/lnd#10937: forward a blinded-route payment when the
recipient identifies the next hop by node ID rather than a short channel
ID. The htlcPacket carries the decoded next hop to the switch, whose
handlePacketAdd resolves the pubkey to the peer's links via getLinks() and
lets the existing non-strict forwarding logic load-balance across the
peer's channels.
outgoingChanID stays a ShortChannelID. It is the persisted CircuitKey and
is set to the selected channel after non-strict selection. The circular
route check filters candidate channels before selection.
Some implementations (e.g. Core Lightning) identify the next hop in a
blinded route by the next node's ID (next_node_id) instead of a short
channel ID. Decode such a hop into a node-ID next hop, the Right of
ForwardingInfo.NextHop, holding the next node's public key. The switch
resolves that key to one of our channels with the peer in a later commit.
BOLT 4 requires a non-final blinded hop to carry exactly one of
short_channel_id or next_node_id, so a hop that sets both is rejected.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Move htlcswitch link-facing channel state boundaries to chanstate.
The link still uses channeldb for forwarding-package persistence, but
channel update callbacks, tower registration, and dust helper channel
types now use the channel state package directly.
Move 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Check configured and advertised forwarding CLTV deltas against max-cltv-expiry
so local configuration and advertised channel policy stay within the same
supported range.
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.
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.
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).
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.
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.
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.
* 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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`).
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.
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.
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.
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.
Introduce the ChainsRecord subtype used by the offer_chains and
invoice_chains TLV fields. Decoding caps the count at maxOfferChains to
bound allocation.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
Replace BreachConfig's concrete ChannelStateDB dependency with
chanstate.Store. The breach arbitrator only needs closed-channel reads
and MarkChanFullyClosed from the channel-state store.
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.
Accept chanstate.Store in ChannelNotifier instead of the concrete
ChannelStateDB. The notifier only fetches open and closed channel
records to populate channel event payloads.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
`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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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/
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Use isSQLDB to explicitly assert the expected outcome per backend:
SQL should succeed with empty results, KV should return
ErrVersionNotSupportedForKVDB.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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_".
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.
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.
This commit updates the `sqldb/v2` package to utilize the new
`MigrationStream` type for executing migrations, instead of passing
`[]MigrationConfig`'s directly.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Convert testEdgeInfoUpdates and testBatchedUpdateEdgePolicy to run
against both v1 and v2 gossip versions, exercising the versioned
Store methods added in prior commits.
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.
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.
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.
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.
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.
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`.
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.
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).
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Update AddEdgeProof to handle both v1 (four separate signatures) and
v2 (single aggregated signature) channel authentication proofs using
the appropriate SQL queries.
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.
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).
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.
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
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
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)
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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>
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
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
In the previous iteration with endorsement
signaling, the recommendation was for the sender to
set it to 1 and that could have had privacy concerns
when first deploying given that the default was to
downgrade the signal to 0. In the latest proposal
the recommended default for both sending and
forwarding nodes is to set `accountable` to 0.
As a result, the dates have been removed given
that there are no privacy risks associated
with relaying the signal with zero values.
This 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.
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
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.
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.
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.
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.
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.
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.
-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
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.
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.
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.
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
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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"
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.
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.
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
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)
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
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.
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.
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.
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.
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.
Introduce `CommitTxBlockHeight` field to the `ResolutionReq` structure
and related methods. This field records the block height where a
commitment transaction has confirmed.
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.
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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:
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."
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:
@ -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: `91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`.
The following key can be used to communicate sensitive information: [`91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/1ecb328bbcf36f76ead67f08008f8db1da07e60e/security@lightning.engineering).