Skip to content

Port the v26.06.10 fixes to master - #9693

Merged
daywalker90 merged 83 commits into
ElementsProject:masterfrom
nGoline:master-port-26.06.10
Nov 5, 2026
Merged

daywalker90 merged 83 commits into
ElementsProject:masterfrom
nGoline:master-port-26.06.10

Conversation

@nGoline

@nGoline nGoline commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

This brings the fixes from the v26.06.10 point release forward to master: 79 commits from the branches the fixes were written on, which were already based on master, plus a unit-test mocks update, the test_onchain_all_dust follow-up the release also carries, a comment fix for check-source-bolt, and one commit adding the 26.06.10 entry to CHANGELOG.md. Authorship is preserved throughout.

These are the same fixes the release ships, source only. As announced in the release notes, the tests for the security fixes are temporarily withheld and will follow in a later PR. Fixes in the release that came from master in the first place (#9575, #9626, #9656 and the other picks) are not repeated here.

What is in it

Channel opens, dual funding and closing

  • dual-funded opens and RBFs that the peer never completes now time out and release our reserved inputs
  • a v2 open requesting option_zeroconf is refused unless zero-conf is allowed for that peer, as for v1 opens
  • a peer dust_limit_satoshis above 5000sat (v1 and v2) or above our reserve (v2) is refused
  • close on a dual-funded open we haven't saved yet, and mutual close of an unconfirmed dual-funded channel, no longer crash
  • commitment updates are refused until both sides sent channel_ready, and a channel with HTLCs is failed instead of forgotten
  • out-of-range legacy closing fee offers and a close feerange with min above max are rejected
  • funder serializes concurrent openchannel2 policy checks, queues RBF funding with its inputs reserved, and keeps the saved UTXOs when an RBF fails
  • openingd no longer indexes the funding PSBT by the peer's prevtx_vout

Splicing

  • channeld no longer crashes when the peer skips commitment_signed on splice finalize
  • a penalty transaction is built for each splice candidate, and hsmd is told the non-opener's balance after a splice
  • inflight commitments are signed with the splice's remote funding key; the funding output is watched on restart while a splice is pending
  • an RBF splice_init must meet the 25/24 feerate floor, and a feerate above our maximum is refused instead of wedging the channel
  • abort_channels no longer fails a channel with a signed splice
  • start_batch with a batch_size above 20 is rejected

onchaind

  • HTLC tx output index is bounds-checked in steal_htlc_tx
  • a racy resolution is closed; THEIR_HTLC is resolved when the peer takes it back, and recorded as theirs only once that's final
  • onchaind is told which commitment closed the channel and which HTLCs are really in it, so an HTLC output goes to the live HTLC, not a removed look-alike
  • hsmd signs our HTLC tx input with SIGHASH_ALL

Gossip, connectd and peers

  • unverified channel_updates are only accepted from the channel peer, and a peer's updates are no longer written to the db one by one
  • cheap gossip checks run before signature verification; gossip messages far larger than any valid one are dropped in connectd and ignored by gossipd
  • reply_channel_range replies are resized once per query; gossip_store is also compacted by size
  • enabled channel_updates for dying channels are ignored, and libplugin-pay disables such channels on matching errors
  • errors and warnings for unknown channels are logged and ignored; connectd sends a single peer_spoke
  • out of file descriptors, pending incoming connections are dropped before peers; a failed accept() on a websocket port no longer crashes
  • DNS resolution never blocks the io loop
  • 0-length per-hop payloads in onion messages are accepted; tlvstream_set_raw initializes meta

RPC, runes, plugins and wallet

  • createinvoice returns error 901 on a duplicate payment hash instead of crashing; signpsbt no longer kills hsmd
  • payment waiters are no longer matched on a freed payment_hash; getlog/check no longer crash after the client hung up
  • runes: pname restrictions fail on ambiguous parameters, and runes over 64KiB are rejected before decoding
  • commando: no use-after-free reusing a command id after an error
  • hook handlers that fail or panic get a safe default reply; hooks pending on an important plugin that dies are replayed on restart
  • --recover is wiped from argv and passed to the re-exec'd lightningd through a pipe; concealed option values aren't printed in option errors
  • offers: empty offer_paths are rejected; recurrence values are bounded and overflow-checked
  • bcli clamps the feerate floor; bookkeeper writes the income CSV only as a plain .csv file
  • the next commitment is persisted before signing it, and the revocation secret is stored before acting on its HTLCs
  • hsmd no longer grants channeld gossip signing permission

Not a straight cherry-pick

  • 33 commits carried their fix and test together. They were picked with only the test edits the release ships (changes to existing tests the fixes require), without the withheld tests. Two of those edits remain: test_onchain_all_dust and test_addgossip.
  • wallet: update run-wallet mocks for channeld_got_revoke sits right after channeld: build a penalty tx for each splice candidate, the commit that needs it, so every commit builds.
  • pytest: don't rely on -txindex in test_onchain_all_dust is the follow-up the release carries for that test on Liquid.
  • common/test: don't start a non-quote comment with BOLT fixes a comment from the onion-message fix that check-source-bolt reads as a spec quote.

Testing

Builds, unit tests and source checks pass on this branch. The same fixes were tested on the v26.06.10 release tree (full unit and integration runs: vanilla, dual-fund, Liquid, postgres, valgrind and sanitizers).

🤖 Generated with Claude Code

Eduard Mikhrin and others added 30 commits November 4, 2026 13:49
createinvoice only had its label checked for duplicates (inside invoices_create()), but payment_hash is UNIQUE in the invoices table too. Calling it with a preimage whose payment_hash was already stored under another label hit that constraint, and db_exec_prepared_v2() treated the failure as fatal, so any RPC caller (including a commando rune allowing createinvoice) could crash the node.

Check for an existing payment_hash before decoding the invoice, as the invoice command already does, and return INVOICE_PREIMAGE_ALREADY_EXISTS (901). This covers both bolt11 and bolt12, and  now reports it too. A matching label still takes precedence and returns 900 with the existing invoice's details, which the offers plugin relies on.

Changelog-Fixed: JSON-RPC:  now returns error 901 for an already-used preimage instead of crashing the node.
A zero-path offer_paths passed decode, then aborted the offers plugin in
current_their_path() on fetchinvoice, taking lightningd down with it.

Changelog-Fixed: offers: no longer crashes on an offer with an empty offer_paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
If the peer answers our splice commitment_signed with tx_signatures, interactive_send_commitments() returns NULL and splice_initiator_user_finalized() dereferenced it. Fail the channel instead, as resume_splice_negotiation() already does.

Changelog-Fixed: channeld no longer crashes when a peer sends tx_signatures instead of commitment_signed during splice finalization.
…eer.

gossipd passes channel_updates for unknown channels to lightningd if they
are signed by the peer which sent them, and lightningd only checked that
this was the channel peer for private channels.  So any node could
overwrite our peer's update for a public channel which wasn't announced
yet (fees, cltv, htlc limits end up in listincoming and our routehints).

Before 24.02 gossipd told us NULL for updates it checked against the
channel_announcement, and we checked the source for all channels.  Go
back to that.

Changelog-Fixed: Protocol: a connected node could overwrite our peer's channel_update for a public channel before it was announced.
Co-authored-by: Cursor <cursoragent@cursor.com>
signpsbt handed the caller's PSBT straight to hsmd, which treats any
libwally signing error as fatal: a PSBT with no outputs (as fundpsbt
and utxopsbt return before the caller adds one), an unknown sighash
type, a taproot input next to an input without UTXO info, or our key
on someone else's input took down hsmd and therefore lightningd.

Refuse PSBTs with no outputs up front, have hsmd return the PSBT
unsigned when libwally refuses it on the signpsbt path, and fail the
command if an input we were asked to sign is still unsigned.

Changelog-Fixed: JSON-RPC: `signpsbt` no longer crashes the node when given a PSBT it cannot sign (e.g. one with no outputs); it returns an error instead.
A peer could send open_channel2 and then go quiet while staying
connected: dualopend waited forever and the UTXOs funder contributed
stayed reserved for 72 blocks.  Give the peer 10 minutes to reach
commitment_signed, then fail the open so funder releases them.

Changelog-Fixed: dual-funding: a peer stalling a channel open no longer keeps our contributed UTXOs reserved.
Same hole as the initial open: after tx_init_rbf the peer can stay
connected and go quiet.  dualopend only reads the peer while it is
negotiating, so it never sees lightningd's depth_reached and the
channel sits in DUALOPEND_AWAITING_LOCKIN even after the original
funding tx confirms, for as long as the peer likes.

Give the RBF the same negotiation timeout.  The channel itself is
fine, so just drop the attempt and disconnect: that unblocks dualopend,
and on reconnect the channel proceeds as normal.

Changelog-Fixed: dual-funding: a peer stalling an RBF can no longer keep our channel from confirming.
time_change and offer_period_start check for value overflows and report
back to caller.

Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Instead of return 0 to indicate errors we use a return boolean pattern.
Allowing 0 answer as a valid time.

Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Add a CLN policy for recurrent offers.
Verify that user input and remote requests comply with the policy.

Changelog-Added: offers: create a CLN policy to constraint recurrence parameters to reasonable values, to avoid user footguns and computation overflows.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-Added: docs: offers and fetchinvoice documentation mention the limits set on recurrence variables stablished by CLN's policy.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-Fixed: dualopend: a v2 channel open requesting 'option_zeroconf' is now refused unless we're configured to accept 0-confirmation channels from that peer, as openingd already does.
Hook handler errors and exceptions in plugin frameworks previously
triggered fatal errors in lightningd or resulted in hangs when a handler
panicked.

In cln-plugin:
- Separate hook and RPC method dispatch paths.
- Add safe fallback mapping across hooks on Err or panic via catch_unwind.
- Replace process::exit(1) on typed hook deserialize schema mismatch with
  returning an Err to allow fallback coercion.

In pyln-client:
- Complete hook_fallbacks table for all 15 hooks (keeping
  commitment_revocation and db_write crash-only).
- Ensure only registered hooks use fallback responses on exception.
- Redact traceback from JSON-RPC error responses on the wire.

Changelog-Changed: Plugins: a hook handler that fails or panics (pyln-client, cln-plugin) now gets a safe default reply instead of crashing or hanging lightningd; commitment_revocation and db_write still stop the node.
listconfigs already conceals --recover, --wallet and the other
OPT_CONCEAL options, but a value that fails to parse is still echoed
back verbatim, e.g. "--recover=<secret>: Not a valid mnemonic, hex, or
codex32 string".  Print "name=..." instead, both on the command line
and in config files.
BOLT ElementsProject#1 requires ignoring an `error` or `warning` whose `channel_id` refers to no existing channel, instead of replying with an error and disconnecting. connectd no longer allocates a subd for such messages, and lightningd now ignores them.
Changelog-Fixed: Avoid closingd crashes on invalid legacy closing fee offers.
A duplicate key, or a name that only differs in punctuation, maps to the same pname. We checked the first one, while the command may use another: plugins take the last duplicate and `plugin` ignores extra names. Now any restriction on such a name fails.

Changelog-Fixed: runes: `pname` restrictions can no longer be bypassed with duplicate or punctuation-variant parameter names.
Changelog-Fixed: onchaind: no longer crashes when an unexpected transaction spends a revoked HTLC output with too few outputs
Changelog-Security: gossipd: stale, duplicate and unknown-node gossip is now rejected before signature verification, so peers can no longer force needless ECDSA work
Changelog-Fixed: Protocol: onion messages with an empty (0-length) per-hop payload are now accepted, as BOLT 4 allows, instead of being silently dropped
jaonoctus and others added 27 commits November 4, 2026 13:54
Grow the pending replies array once per message rather than once per
scid, use size_t for the counts, and drop the query if the accumulated
replies exceed a sane limit.

Changelog-Fixed: gossipd: more efficient handling of large reply_channel_range replies.
bkpr-dumpincomecsv passed csv_file straight to fopen(.., "w"), so it
could name, and truncate, any file the node can write.  The file goes
in the node's network directory, next to the node's own files, so
restricting it to a name alone isn't enough either.

Accept only a plain filename ending in .csv (no directory, not hidden),
and open it without following a symlink.  Rewriting an existing .csv
keeps working.

Changelog-Changed: bookkeeper: `bkpr-dumpincomecsv` `csv_file` must now be a plain filename ending in `.csv`; it is written in the node's network directory, and never through a symlink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer_got_revoke() applied the HTLC changes which a revoke_and_ack
locks in -- forwarding the final accepts, settling updates and
failing fails -- before it had checked the revocation secret.  If
wallet_shachain_add_hash() rejects that secret we fail the channel
anyway, so it must not act on them first.

Run the shachain and revokenum checks before acting on the HTLCs.
Nothing in between touches HTLC state, and crash-consistency is
unchanged: sd_msg_read() wraps every channeld message in a single DB
transaction, so a crash replays the whole message or nothing.

Reported-by: Erick C (@erickcestari)
…hannel

After an RBF the channel has more than one candidate funding tx.  After
a mutual close we watch the ones we didn't close on, in case they
confirm.  To build that watch we need the peer's funding key, and we
were reading it from a field that only splices fill in.  For RBF it is
NULL, and we crashed on it:

    lightningd: FATAL SIGNAL 6
    bitcoin/pubkey.c:88 (pubkey_cmp)
    bitcoin/script.c:155 (bitcoin_redeem_2of2)
    lightningd/peer_control.c:530 (drop_to_chain)
    lightningd/closing_control.c (peer_closing_complete)

RBF never changes the funding keys, so use the channel's own copy of
the peer's key instead.

Changelog-Fixed: lightningd: fix a crash on cooperative close of an unconfirmed dual-funded channel.
A channel_announcement, channel_update or node_announcement is a
few hundred bytes.  Add generous upper bounds (1024, 1024 and 2048
bytes) and ignore anything larger, before we do any work on it.

Changelog-Fixed: gossipd: ignore gossip messages far larger than any valid one.
Signed-off-by: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
We compact the gossip_store when dead records outnumber live ones
4:1.  That only works if records are similar in size, and they are
not: a node_announcement can be 2048 bytes, a channel_update ~140.

So remember the store size after the last compaction (at startup,
file size minus deleted records) and also compact once the store is
5x that.  A failed compaction resets the baseline so we don't retry
on every message.

Changelog-Fixed: gossipd: the gossip_store is also compacted based on its size, not just its record count.
Signed-off-by: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
gossipd ignores gossip messages far larger than any valid one, but
connectd was still queueing them for gossipd.  Apply the same bounds
in connectd, before the message is queued.

Changelog-Fixed: connectd: gossip messages far larger than any valid one are dropped before they are queued for gossipd.
Signed-off-by: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
setup_peer() re-arms the chain watches for every channel on startup.  For
a channel in CHANNELD_AWAITING_SPLICE it only watched the splice
inflights, so after a restart nothing was watching the channel's current
funding output.  That output stays live until a splice confirms, and if
it is spent by a commitment instead we never noticed: onchaind was not
started and the channel was stuck in CHANNELD_AWAITING_SPLICE.

Watch the funding as we do in every other state, so the channel has the
same watches it had before the restart.

Changelog-Fixed: lightningd: a channel with an unconfirmed splice now notices if its funding output gets spent after a restart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolving a DNS address announced in a node_announcement used to call
getaddrinfo() directly, on connectd's single io thread.  A slow or
unresponsive nameserver held up every peer on the machine for the
duration of the lookup: ~10s per query with glibc defaults, enough to
trip the 60s ping-liveness check and disconnect all peers.

getaddrinfo() now runs in a forked child which writes the results
back over a pipe; connectd reads the pipe from its io loop and
resumes the connect attempt from a loop callback, so the io thread
never waits on resolution.  Keeping libc for the actual lookup
preserves /etc/hosts, search domains, systemd-resolved and
AI_ADDRCONFIG ordering exactly, and keeps DNS packet parsing out of
connectd itself.

A 30-second cap covers each lookup end to end (a wedged child is
killed), and at most ten resolvers run at once: a gossip-driven
reconnect storm queues the excess FIFO rather than forking without
bound.  Before resolving, the child closes every fd it inherited: a
wedged child must not keep peer connections alive, nor pin EOF on
another lookup's pipe with its copy of that write end.

The connect RPC resolved a host name the same way, with a blocking
getaddrinfo() in lightningd's main loop, and ignored --disable-dns.
It now hands the unresolved name to connectd as a DNS address, which
goes through the same asynchronous resolver and honours
--disable-dns (unless a proxy resolves names for us).

Reported-by: Vincenzo Palazzo
Changelog-Fixed: connectd: a stalling DNS name in a peer's announced address no longer freezes all peer traffic; resolution is now done asynchronously.
Changelog-Changed: `connect` with a host name now resolves it asynchronously in connectd, and fails with `--disable-dns`.
Upon timing out an upstream incoming HTLC, we were setting the
resolution to be ignored, since the funds will presumably be returned
to the peer, not us. However, since the HTLC timeout and the internal
resolution do not line up perfectly, due to the security margin, there
is a chance for a late preimage coming in, resolving the HTLC in the
other direction, i.e., we now own it. If that resolution comes in
after we have internally marked the output as ignored, we would not
clean up our prior decision, leaving the HTLC stranded, but
recoverable.

This just ensures that we clear our internal resolution too, when the
downstream preimage comes in, so that we can use the preimage to grab
the funds in the HTLC.

Changelog-Fixed: onchaind: Fixed a race between preimage return from downstream to upstream
The non-revoked THEIR_HTLC arm of output_spent() only annotates: it has
always relied on the "no witness elements" fallback in tx_new_depth() to
resolve the output once we reach the timeout depth.  But handle_preimage()
replaces that ignore-proposal with a real spend, which fills in the witness
elements, and kills the fallback for that output.

So on the losing side of the same race the previous commit fixes -- their
HTLC-timeout confirms after we decided to claim it -- nothing ever resolves
the output: wait_for_resolved() spins forever, we never send
all_irrevocably_resolved, and the channel is never forgotten.  There's a
second ordering with the same outcome: we see their spend first (and skip
it, as the output is resolved), then the preimage arrives and clears that
resolution.

Resolve it in the spend arm, and don't try to claim an output we watched
them spend: there is nothing left to grab, and proposing a spend of it is
exactly what strands us.  That's the *only* case where we decline a
preimage: an HTLC we merely gave up on is always still claimable, which is
what the enum makes explicit.

Changelog-Fixed: onchaind: Don't wait forever if the peer takes back an HTLC we were trying to claim with a late preimage

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When an incoming HTLC timed out we told the bookkeeper immediately that it
had gone back to the peer.  If a late preimage then let us claim it after
all, we booked it *again*, this time to ourselves: the bookkeeper never
reverses an event, so the commitment output ends up with two deposits.
find_txo_set() then builds two pairs for one output, the pairs ==
closed_count check in account_onchain_closeheight() fails, and the channel
is never marked resolved.

We can't record it any earlier than we know it, so record it later instead:
when we see their spend confirm, or, for an output nobody ever spent, on
our way out of wait_for_resolved(), where no preimage can reach us any
more.  Note we can't record it in the spend arm alone: propose_ignore()
uses cltv_expiry while their spend can only confirm after it, so we always
give up on the output before they take it, and the deposit would go
unrecorded (which leaves the channel unresolved just the same).

As a side-effect this also covers propose_ignore()'s immediate path, taken
when the commitment confirms at or after the expiry, which never recorded
anything at all.

Changelog-Fixed: bookkeeper: An incoming HTLC we claim with a late preimage after it timed out is no longer booked both to the peer and to us

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three bare 100s (the BOLT ElementsProject#5 quote above the first one explains what
they are).  Taken from Rusty's branch.

Changelog-None

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XZZWMuBhXrq41u9HhxVBM
If lightningd can't make our claim on a THEIR_HTLC pay (a small HTLC,
or a fee spike since the commitment was signed) it says so in the
spend_created reply, and handle_spend_created() ignores the output.
That's giving up just as a timeout is, but it left the fate as
THEIR_HTLC_UNDECIDED, so neither the spend arm nor the exit recorded
the htlc_timeout deposit: bkpr never saw the HTLC leave, and the
channel was never resolved.  master booked it exactly once here, so
this was a regression of "onchaind: only record an HTLC as theirs once
that's final".

The lowballed spend can also confirm anyway.  We used to skip it at the
already-resolved check and book nothing (and now would have booked it
as theirs).  Record it as ours, and re-anchor the resolution on the
spending tx: that's what actually resolved the output, so the 100-block
clock for irrevocable resolution runs from its confirmation.  A lowball
tx can mine long after the commitment itself is 100 deep, and counting
from the commitment would let us forget the channel while the spend is
a block deep and could still be reorged out.  This delays forgetting a
channel we gave up on until any spend we'd written off matures;
test_onchain_all_dust checks that.

Only claims lightningd builds itself can be lowballed: pre-signed
HTLC-success/-timeout txs always come back as worthwhile.  So the test
has l1 close, and forces the lowball with a fee spike after the
commitment confirms.  One variant then has l1 take the HTLC back, the
other mines the lowballed claim directly (bitcoind won't relay it,
since lightningd's weight estimate for a fulfill spend leaves out the
preimage and so undershoots the relay minimum at the floor; that's a
separate bug).

The tests above restart the node that wrote the claim off, after
that claim's confirmation: propose_resolution_to_master() waits for the
spend_created reply before anything else is processed, so replay
re-derives the same ignore decision, re-matches the mined tx against
our proposal, and re-books through this branch -- the tests assert
bookkeeper sees it exactly once and depth counts from the confirmation
all the same.

Changelog-None

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XZZWMuBhXrq41u9HhxVBM
…g key

The peer may use a new funding_pubkey for a splice, so the splice output
is a 2-of-2 of our key and that one.  We signed an inflight's commitment
with the channel's current remote funding key, which doesn't match it.

Use the inflight's splice_remote_funding, the key we already watch the
splice output with, in drop_to_chain() and dev-sign-last-tx.

Changelog-Fixed: lightningd: unilateral close of a channel with a pending splice now works when the peer used a new funding key for it.
With a splice pending, channeld sends splice_init as tx_init_rbf, and
BOLT 2 requires that feerate to be at least 25/24 of the previous one.
We didn't check, so we proposed RBFs a peer such as Eclair rejects.

Refuse a lower feerate at the RPC, and raise the default up to the
floor.  test_splice_rbf_duplicate_inflight's RBF now has to clear it.

Changelog-Fixed: splice_init: RBF of a pending splice now uses at least 25/24 of its feerate, which peers such as Eclair require.
Co-authored-by: Cursor <cursoragent@cursor.com>
splice_feerate() is already capped at our_feerate_max(), so raising it to
25/24 of a splice that is pending near that cap goes over.  channeld
refuses that too, but only after setting peer->splicing, which it does
not clear on that error: every later splice_init then failed with "Can't
start two splices on the same channel at once" until channeld restarted.
An explicit feerate above the cap did the same.

Refuse it in json_splice_init instead, as openchannel_bump does.

Changelog-Fixed: splice_init: a feerate above the most we'll pay no longer leaves the channel unable to splice until channeld restarts.
Co-authored-by: Cursor <cursoragent@cursor.com>
abort_channels passed the last inflight to splice_abort() even once we
had sent tx_signatures for it, which failed the channel.

If a negotiation is in progress, that inflight is the splice it is
RBFing: abort the negotiation and keep the inflight.  If we're quiescent
(after stfu_channels), tx_abort is still how we leave, and the inflight
is kept too.  Otherwise there is nothing to abort, so don't send
tx_abort at all: BOLT 2 forbids it after tx_signatures, we're no longer
quiescent, and splice_abort() would throw away whatever the peer sends
while it waits for the ack.

Changelog-Fixed: abort_channels no longer fails the channel when a splice we already signed is pending.
Co-authored-by: Cursor <cursoragent@cursor.com>
Changelog-Fixed: Protocol: channeld now rejects a 'start_batch' with 'batch_size' above 20, as the spec requires, so a peer can no longer stall the channel or use up memory
… theirs.

onchaind works this out when it starts (it compares the txid to the one
lightningd told it we broadcast).  lightningd only gets the commitment
number back, which doesn't say whose commitment it is.  Add a flag to
init_reply so it knows.

lightningd reads the flag but doesn't use it yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
When a channel closes, lightningd gives onchaind a list of HTLCs that
might be in the commitment tx.  The list is a bit too generous: an HTLC
removed just before the close is still on it.

That's harmless until two of those HTLCs share a payment hash, amount
and cltv: onchaind can't tell them apart, and the commitment tx may
contain only one of them.  We still track each live HTLC's state, which
says whether it's in the latest commitment on each side, or in the
peer's previous commitment while that one is not yet revoked.  Send
that as a `present` flag with each HTLC.

onchaind stores the flag but doesn't act on it yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
When two HTLCs on the list matched the same output, onchaind took the
first one in cltv order.  If a removed HTLC had the same payment hash,
amount and cltv as a live one, it could win, and the live HTLC was
reported missing.  lightningd then failed it upstream right away, while
the peer could still claim the output onchain with the preimage.  A
forwarding node loses the whole HTLC.

Same bug as 027e97e (trimmed HTLC as the look-alike), different decoy.

Fix: when HTLCs tie on cltv, sort the ones lightningd says are present
first.  Matching is still one output to one HTLC, so a commitment that
really does have two identical HTLCs works as before.

Changelog-Fixed: onchaind: an HTLC removed just before a unilateral close, with the same payment hash, amount and cltv as a live one, could be mistaken for it onchain, causing us to fail the live HTLC upstream while the peer could still claim it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Changelog-Fixed: commando and rune RPCs reject oversized runes instead of spending quadratic time decoding them.
The test elementsd runs without -txindex, so getrawtransaction on the
mined HTLC spend failed on liquid:

    No such mempool transaction. Use -txindex or provide a block hash
    to enable blockchain transaction queries.

Pass the hash of the block that mined it.

Changelog-None
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hbJ3AeummizNQSXoHKhYo
check-source-bolt reads any "BOLT ElementsProject#4" comment as a spec quote, and this
one isn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 893ec0f55d77825def0b805536e5693d65bff2f4)
Changelog-None
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hbJ3AeummizNQSXoHKhYo
@nGoline
nGoline requested a review from cdecker as a code owner November 4, 2026 17:00
@daywalker90
daywalker90 merged commit 4fc822f into ElementsProject:master Nov 5, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants