Skip to content

Port release 26.06.8 to master - #9596

Merged
nGoline merged 9 commits into
ElementsProject:masterfrom
sangbida:port-release-26.06.8-to-master
Oct 2, 2026
Merged

nGoline merged 9 commits into
ElementsProject:masterfrom
sangbida:port-release-26.06.8-to-master

Conversation

@sangbida

@sangbida sangbida commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Amperstrand and others added 9 commits October 2, 2026 14:38
invoice's `expiry` parameter is an unclamped param_u64, and far-future
values break the daemon in two different ways:

- expiry >= 2^60 needs more bits than push_varlen_field() can encode in
  the bolt11 `x` field, so bolt11_encode() aborts the whole daemon
  (FATAL SIGNAL 6).

- far below that (anywhere past ~584k years), the invoice expiration
  timer's nanosecond-grain u64 counter overflows: install_expiration_timer()
  arms a timer that reads as already due, trigger_expiration() finds
  nothing expired, re-arms, and the daemon busy-loops at 100% CPU with
  the RPC reply left racing the storm.

Refuse at the parameter stage instead: expiry >= 2^32 seconds (~136
years) returns JSONRPC2_INVALID_PARAMS, keeping a wide margin under both
limits. 2^32 - 1 still works.

Changelog-Fixed: lightningd: fix crash (`FATAL SIGNAL 6`) and a 100% CPU busy-loop when calling `invoice` with an `expiry` too far in the future (now refused above 2^32 seconds).
(cherry picked from commit 009caeb)
A crafted bolt11 can carry expiry >= 2^32 past the invoice RPC's
gate: the expiry field is a varint and decode does not require a
valid signature, so createinvoice fed b11->expiry straight into
invoice creation -- hitting the same bolt11_encode() 60-bit abort
and expiry-timer overflow as the invoice path. Same bound, same
message, placed before the re-encode. Pointed out in the PR thread.

Changelog-Fixed: lightningd: refuse bolt11 `expiry` above 2^32 seconds in `createinvoice` too, instead of crashing or busy-looping.
Signed-off-by: Amperstrand <amperstrand@localhost>
(cherry picked from commit 5f433f0)
The createinvoice expiry test hardcoded the lnbcrt prefix, but
liquid-regtest invoices carry the ert HRP (chainparams), so the
crafted string died at the bech32 parse there ('invalid bech32
string') before the bound check -- the liquid CI arm has been
failing on it. Take the HRP from a real invoice on the running
network instead; both arms pass locally now.

Changelog-None: test-only
Signed-off-by: Amperstrand <amperstrand@localhost>
(cherry picked from commit ebeaf77)
…e commitment

With every fee estimate at the floor, the opener's closing fee range is a
single value.  Each output is rounded down to whole satoshis, so the msat
remainders end up in the fee and the closing transaction pays one satoshi
more than the fee both sides agreed on.  lightningd rejects that
transaction as above its maximum, keeps the commitment as last_tx, and
the close proceeds to broadcast the commitment while reporting a mutual
close.

test_closing_fee_rounding_at_ceiling pins both nodes at the floor, leaves
remainders of 999 and 1 msat, closes from the opener, and checks that the
transaction returned by close is a two-output closing transaction paying
the agreed fee plus one satoshi, and that both nodes see the output after
one block.  Marked xfail until the following commits.

Changelog-None.

(cherry picked from commit 0471b3b)
closing_fee_is_acceptable checked the fee the closing transaction pays
against the maximum derived from the unilateral feerate.  That fee is
larger than the fee_satoshis both sides agreed on whenever the outputs'
msat remainders were rounded away or an output was trimmed as dust.
With every estimate at the floor, closingd's minimum and maximum are the
same value, so any rounding put the transaction one satoshi over the
maximum and lightningd rejected it.  The rejection left the commitment
as last_tx and the close broadcast it while reporting a mutual close.

Derive the negotiated fee from the transaction: our rounded-down balance
minus the output paying our shutdown script.  closingd subtracts
fee_satoshis from the opener's output, and the maximum only applies when
we are the opener, so that difference is exactly the fee we agreed to
pay.  Bound that instead.  The minimum is still checked against the fee
the transaction actually pays, which is what relay depends on.  If our
output was trimmed the whole fee is ours and is bounded as before.

Changelog-Fixed: lightningd: a mutual close at the fee ceiling no longer falls back to broadcasting the commitment because of satoshi rounding.
Fixes: ElementsProject#9495
(cherry picked from commit be5b625)
…ling back to the commitment

closingd negotiates within the feerange given to `close`, and lightningd
hands it the range minimum as its floor.  lightningd's own acceptance
check does not use that minimum: it checks the agreed fee against the
floor derived from its fee estimates.  When the estimates sit above the
range, the agreed fee is rejected as too low, the commitment stays as
last_tx, and the close broadcasts it while reporting a mutual close.

test_closing_feerange_below_estimates opens at the floor, raises the
opener's estimates, closes with a feerange pinned at the floor, and
checks that the transaction returned by close is a two-output closing
transaction at the agreed fee.  Marked xfail until the following commit.

Changelog-None.

(cherry picked from commit 9c0b9c2)
…g fee

peer_start_closingd hands closingd the minimum of the feerange given to
`close` as its floor, so closingd negotiates down to it.  The acceptance
check in closing_fee_is_acceptable kept using the floor derived from the
fee estimates, so with estimates above the range the agreed fee was
rejected as too low, the commitment stayed as last_tx, and the close
broadcast it while reporting a mutual close.

Use the range minimum as the floor there too, as calc_max_close_feerate
already does for the maximum.  Both ends of the check now match the
bounds closingd negotiated within.

Changelog-Fixed: lightningd: `close` with a feerange below the fee estimates no longer falls back to broadcasting the commitment.
(cherry picked from commit 118bf9c)
…cted

When lightningd rejects a closing fee, closingd never learns it: the
reply only carries a txid, which closingd uses for its billboard.  It
agrees to the offer, reports the close complete, and lightningd
broadcasts last_tx, still the commitment, as if it were the mutual
close.

The previous commits removed the known reasons for a rejection, so add
--dev-reject-closing-fee, which makes lightningd reject every closing
fee the peer offers.  test_closing_rejected_fee_fails_negotiation runs
it on the non-opener: the opener's close must end in a unilateral close
at its timeout, and the rejecting node must never agree to a fee or
broadcast anything.  Marked xfail until the following commit.

Changelog-None.

(cherry picked from commit 24d4335)
…sing fee

lightningd's reply to closingd_received_signature carried only a txid.
closingd used it for the billboard and went on to agree to the offer,
so when lightningd had rejected the fee the close still completed and
drop_to_chain broadcast last_tx, which was still the commitment, while
the close command reported a mutual close.

Add the verdict to the reply.  On a rejection lightningd logs it at
UNUSUAL, and closingd sends the peer a warning and exits instead of
agreeing, the same way it handles a fee range with no overlap.  The
channel stays in CLOSINGD_SIGEXCHANGE: negotiation restarts on
reconnect with lightningd's current bounds, and the close command's
timeout decides when to close unilaterally.

Changelog-Fixed: lightningd: a closing fee lightningd rejects fails the negotiation instead of broadcasting the commitment as a mutual close.
(cherry picked from commit a944beb)
@nGoline
nGoline merged commit a764f9c into ElementsProject:master Oct 2, 2026
38 of 45 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.

3 participants