581 Commits

Author SHA1 Message Date
ThomasV 5a31bf6aa0 Merge pull request #10463 from f321x/jit_2
lnwallet: zeroconf/just-in-time improvements and tests
2026-04-28 10:16:34 +02:00
ThomasV 7c433c5645 rm 'received orphan channnel' log line (too verbose) 2026-04-23 09:29:08 +02:00
SomberNight 46eadbf442 lnpeer: channel_reestablish: further restrict states for msg handler
re REQUESTED_FCLOSE and WE_ARE_TOXIC (as per f321x):
> There is no reason for the peer to send channel_reestablish after we
> have sent the force close request (error) and I assume we don't
> want to give surface to the peer to attempt finding out if we really lost state?

re FORCE_CLOSING:
the peer might not have realised we started force-closing but we probably don't want to run the message-handler even in that case
2026-04-22 15:57:18 +00:00
SomberNight 21946e1e87 lnpeer: channel_reestablish: split "they_are_ahead" into ctn vs revnum 2026-04-18 17:23:35 +00:00
SomberNight 4a14feffd4 lnpeer: chan_reest: clarify my_current_per_commitment_point is ignored
`my_current_per_commitment_point` was only used prior to option_static_remotekey
2026-04-14 14:18:14 +00:00
f321x a06c8bacc3 lnpeer: don't signal OPTION_ZEROCONF_OPT to untrusted peer
Only signal `OPTION_ZEROCONF_OPT` to peers if we either:
1. Have no trusted peer configured (assuming that we are LSP)
2. Have a trusted peer configured, and the peer we are connecting
   to is this trusted peer.

Otherwise peers that are LSPs but are not the clients trusted LSP
might try to open a channel to the client but it would get rejected.
2026-03-26 17:39:05 +01:00
f321x 85356e5544 lnwallet: make jit fees configurable, add mining fees
Make the just in time channel fees and channel size
configvars, as in practice not every provider would
use the same hardcoded fees or channel sizes.
Add the mining fees required for the funding transaction on top of the
opening fees to prevent opening channels at a loss in a higher
fee environment.
2026-03-26 17:39:03 +01:00
f321x 297aed99f0 lnpeer: check just-in-time channel opening fee
Check the just-in-time channel opening fee when receiving an incoming
channel opening.
2026-03-26 17:38:49 +01:00
SomberNight 7e3af72ad6 lnpeer: maybe_send_commitment: impl batching updates 2026-03-11 17:07:23 +00:00
SomberNight 93f0452406 lnpeer: simplify where maybe_send_commitment() is called
The typical flow of an update is:
---UPDATE--->
--- SIG  --->
<--REVACK----
<-- SIG  ----
---REVACK--->

It makes sense to try to send a sig ("commitment_signed") right after we send an update.
It also makes sense right after we send revack.

Besides those times, we could call "maybe_send_commitment" at *any* time, that is safe, and depending on other call locations, it might be an optimisation, however it is not needed.

In particular it is unclear why we had those calls when we *receive* updates (and only for certain types of updates - not consistently).
2026-03-11 17:07:19 +00:00
SomberNight 0b2c7a8a38 lnsweep: safer maybe_reveal_preimage_for_htlc, add "is_preimage_public"
"When should we reveal preimages onchain?"
This commit tries to simplify the thinking by making the observation:
- we can reveal preimages (actually in any context) if they are already public
- a preimage is public if any other lightning node knows it besides us
  - if we learn the preimage from another LN node, it is public
  - if we send update_fulfill_htlc, it becomes public
  - if we see a preimage onchain, it is public

- in lnsweep._maybe_reveal_preimage_for_htlc:
  - partial mpp check is not relevant if preimage is already public
  - let's just always do KeepWatchingTXO, for sanity/safety

Co-authored-by: ThomasV <thomasv@electrum.org>
2026-02-24 17:49:15 +00:00
f321x ddb01f5355 lnpeer: don't save our own channel update as remote upd
I noticed CLN is sending our own channel update to us on
reestablishment, we then assume it to be the remote nodes
update and try to verify the signature against their pubkey
which fails and throws `InvalidGossipMsg`.

This adds a check preventing us from trying to save our own
channel updates as remote update.
2026-02-23 17:12:15 +01:00
ThomasV 952737594b announcement_signatures: add more early returns 2026-02-17 08:57:49 +01:00
ThomasV b03d6a478d gossip: broadcast channel updates along with channel announcements
The channel update in mark_open is only sent once, and only to the
channel peer. In addition, it seems to be discarded by Eclair.
2026-02-09 11:00:16 +01:00
SomberNight 0ae10b2a7b lnpeer: log name of wallet file in each line
- if multiple LN-enabled wallets are open, need to know which peer is for which wallet
- note: LNGossip is a singleton
  - if a wallet is named LNGossip, can't distinguish. I think that's ok.

compare log lines:
before:
```
84.82 | I | lnpeer.Peer.[LNWallet, 034cc6216f-f8dcaa6e] | Disconnecting: GracefulDisconnect('Failed to initialize: TimeoutError()')
17.97 | D | lnpeer.Peer.[LNGossip, 0259d4116d-1618547b] | Sending INIT
```
after:
```
 5.80 | D | lnpeer.Peer.[test_segwit_2, 038863cf8a-fd53ef9c] | Sending CHANNEL_READY
 5.92 | D | lnpeer.Peer.[LNGossip, 038863cf8a-6286ffd4] | Received INIT
```
2026-01-19 16:03:42 +00:00
user e81ac4b7cd lnpeer: followup #10413
Save the updated htlc set in `Peer._fulfill_htlc_set` and
`Peer._fail_htlc_set()` only after the loop iterated through all htlcs.
This potentially improves performance, especially considering that
writing the db can take >100 ms for larger wallets without partial
writes.
2026-01-16 12:16:23 +01:00
SomberNight c37b844f66 lnutil: change ReceivedMPPStatus.htlcs to frozenset, i.e. immutable
As ThomasV says:

> ReceivedMPPStatus is a Namedtuple, which is immutable, but it contains
> a mutable field. Since ReceivedMPPStatus is not a StoredObject,
> no patch will be created when the htlcs list is modified, and we may
> end up not saving the change to disk if partial writes are enabled.

patch taken from https://github.com/spesmilo/electrum/pull/10395#pullrequestreview-3634244541
closes https://github.com/spesmilo/electrum/pull/10395

Co-authored-by: f321x <f@f321x.com>
2026-01-15 15:59:14 +00:00
SomberNight b292c027c3 lnpeer: move make_local_config to LNWallet
no functional changes
2026-01-05 15:55:41 +00:00
SomberNight 1006e8092f lnworker: split LNWallet and LNWorker: LNWallet "has an" LNWorker
- LNWallet no longer "is-an" LNWorker, instead LNWallet "has-an" LNWorker
- the motivation is to make the unit tests nicer, and allow writing unit tests for more things
  - I hope this makes it possible to e.g. test lnsweep in the unit tests
  - some stuff we would previously have to write a regtest for, maybe we can write a unit test for, now
- in unit tests, MockLNWallet now
  - inherits LNWallet
  - the Wallet is no longer being mocked
2026-01-05 15:55:31 +00:00
ghost43 bdcd3f9c7c Merge pull request #10364 from f321x/test_dont_settle_htlcs_forwarding
lnpeer/lnworker: check dont_settle_htlcs when forwarding
2025-12-30 16:13:16 +00:00
f321x 14977e4cee lnpeer: fix callback type hint
asyncio.create_task expects a Coroutine, not all Awaitables are
Coroutines.
2025-12-19 16:37:23 +01:00
f321x ff61596598 lnpeer: fix callback exception handler
The done_callback for the callback tasks in _run_htlc_switch_iteration
tried to access mpp_sets by key but they might already have been deleted
when the callback is called, causing an KeyError. Instead forward the
exceptions to the crash reporter so we get notice of them and they get
logged correctly.

```
20251219T131356.946565Z |    ERROR | asyncio | Exception in callback Peer._run_htlc_switch_iteration.<locals>.<lambda>(<Task finishe.../util.py:1773>) at /home/user/code/electrum-fork/electrum/lnpeer.py:2907
handle: <Handle Peer._run_htlc_switch_iteration.<locals>.<lambda>(<Task finishe.../util.py:1773>) at /home/user/code/electrum-fork/electrum/lnpeer.py:2907 created at /usr/lib64/python3.14/asyncio/events.py:94>
source_traceback: Object created at (most recent call last):
  File "/usr/lib64/python3.14/threading.py", line 1082, in _bootstrap_inner
    self._context.run(self.run)
  File "/home/user/code/electrum-fork/electrum/util.py", line 1145, in run_with_except_hook
    run_original(*args2, **kwargs2)
  File "/usr/lib64/python3.14/threading.py", line 1024, in run
    self._target(*self._args, **self._kwargs)
  File "/home/user/code/electrum-fork/electrum/util.py", line 1705, in run_event_loop
    loop.run_until_complete(stopping_fut)
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 706, in run_until_complete
    self.run_forever()
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 677, in run_forever
    self._run_once()
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 2038, in _run_once
    handle._run()
  File "/usr/lib64/python3.14/asyncio/events.py", line 94, in _run
    self._context.run(self._callback, *self._args)
Traceback (most recent call last):
  File "/usr/lib64/python3.14/asyncio/events.py", line 94, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnpeer.py", line 2909, in <lambda>
    f"{self.lnworker.received_mpp_htlcs[pk]=}", exc_info=t.exception()) if t.exception() else None
       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^
KeyError: '0000980000010001:1'
```
2025-12-19 16:37:15 +01:00
SomberNight aab22a237b ln: require LnFeatures.OPTION_CHANNEL_TYPE as bolts now mandate it
This simplifies some code.

following https://github.com/lightning/bolts/commit/9d456b1c4a6c8e05a6b5b5edbc6c10f7b4b8e4de
2025-12-18 16:43:19 +00:00
f321x 060cbff8da lnpeer: dont remove from dont_settle_htlcs when failing
Don't remove a payment hash from LNWallet.dont_settle_htlcs and
dont_expire_htlcs if we are failing it.
We might see another htlc with the same payment hash and
should still not settle or expire it.
2025-12-15 10:35:29 +01:00
f321x 9906eb42ac lnpeer: forwarding: dont release preimage if dont_settle_htlc
If a payment hash is in LNWallet.dont_settle_htlcs the preimage
shouldn't be released even when just forwarding.
2025-12-12 15:44:48 +01:00
f321x 1ebb937d46 lnpeer: decode_short_ids: check length of short ids
- if `encoded_short_ids` does not decode into a whole number of `short_channel_id`:
  - MAY send a `warning`.
  - MAY close the connection.
https://github.com/lightning/bolts/blob/0cf21511a781c295b9374aeaef37cf9c4d193502/07-routing-gossip.md?plain=1#L674

# Conflicts:
#	electrum/lnpeer.py
2025-12-11 15:44:11 +01:00
f321x 380d7edea2 lnpeer: remove support for zlib compression
Must not be used anymore according to bolt 7:
https://github.com/lightning/bolts/blob/0cf21511a781c295b9374aeaef37cf9c4d193502/07-routing-gossip.md?plain=1#L600
2025-12-11 15:43:45 +01:00
SomberNight c465f7c3e0 lnworker/lnpeer: don't use lnworker.channels.get(chan_id)
- lnworker.channels takes a copy of the whole dict, to make it thread-safe
- in LNWallet class, can just use self._channels.get(chan_id)
- otherwise there is lnworker.get_channel_by_id
- same for lnpeer.channels.get and lnpeer.get_channel_by_id
2025-12-10 16:14:31 +00:00
ghost43 6ceb4ad71f Merge pull request #10351 from f321x/jit_htlc_switch_fixes
lnpeer/lnutil: fail mpp if we didn't signal mpp in invoice
2025-12-10 15:56:23 +00:00
f321x c34efce984 lnchannel: allow deleting unfunded incoming channels
We tried to delete incoming channels that didn't get funded after
lnutil.CHANNEL_OPENING_TIMEOUT, however an assert prevented this:

```
  3.63 | E | lnwatcher.LNWatcher.[default_wallet-LNW] | Exception in check_onchain_situation: AssertionError()
Traceback (most recent call last):
  File "/home/user/code/electrum-fork/electrum/util.py", line 1233, in wrapper
    return await func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnwatcher.py", line 117, in check_onchain_situation
    await self.update_channel_state(
    ...<5 lines>...
        keep_watching=keep_watching)
  File "/home/user/code/electrum-fork/electrum/lnwatcher.py", line 135, in update_channel_state
    chan.update_onchain_state(
    ~~~~~~~~~~~~~~~~~~~~~~~~~^
        funding_txid=funding_txid,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<2 lines>...
        closing_height=closing_height,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        keep_watching=keep_watching)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnchannel.py", line 341, in update_onchain_state
    self.update_unfunded_state()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/user/code/electrum-fork/electrum/lnchannel.py", line 382, in update_unfunded_state
    self.lnworker.remove_channel(self.channel_id)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnworker.py", line 3244, in remove_channel
    assert chan.can_be_deleted()
           ~~~~~~~~~~~~~~~~~~~^^
AssertionError
```
2025-12-10 13:20:32 +01:00
f321x 183d426e93 lnpeer: fail htlcs if we get unwanted mpp
Fail incoming htlcs if we receive a payment consisting of multiple parts
if we signaled to not want mpp in the invoice.
2025-12-10 10:36:39 +01:00
f321x 5be598b808 lnworker: use channel_id instead of scid in ReceivedMPPHtlc
Store the channel id instead of the scid in ReceivedMPPHtlc.
The scid can be None, in theory even for multiple channels at the same
time. Using the channel_id which is always available and unique seems
less error prone at the cost of temporarily higher storage requirements
in the db for the duration of the pending htlcs.

Alternatively we could use the local scid alias however using the
channel_id seems less complex and leaves less room for ambiguity.
2025-12-09 14:44:11 +01:00
ThomasV da998150ac lnpeer: deduct JIT fees also for trampoline 2025-12-06 10:33:24 +01:00
f321x fb566eb59e lnpeer: deduct jit channel fees from total amount
Deduct the just in time channel opening fees from the total amount so
htlcs don't get timed out if they come from a just in time channel with
opening fee.

Related: https://github.com/spesmilo/electrum/pull/9584
2025-12-06 10:14:07 +01:00
f321x 923d48f9db lnworker: differentiate PaymentInfo by direction
Allows storing two different payment info of the same payment hash by
including the direction into the db key.
We create and store PaymentInfo for sending attempts and for requests (receiving),
if we try to pay ourself (e.g. through a channel rebalance) the checks
in `save_payment_info` would prevent this and throw an exception.
By storing the PaymentInfos of outgoing and incoming payments separately in
the db this collision is avoided and it makes it easier to reason about
which PaymentInfo belongs where.
2025-12-01 18:39:56 +01:00
f321x 16ed7e666c lnpeer: use INVALID_ONION_VERSION for unparsable onions
Use the `OnionFailureCode.INVALID_ONION_VERSION` (BADONION | PERM | 4)
code when sending back `update_fail_malformed_htlc` as just sending a plain
`BADONION` is not explicitly mentioned as correct in the spec.
2025-11-27 17:58:49 +01:00
f321x abc469c846 lnworker: split dont_settle_htlcs
Splits `LNWallet.dont_settle_htlcs` into `LNWallet.dont_settle_htlcs`
and `LNWallet.dont_expire_htlcs`.

Registering a payment hash in dont_settle_htlcs will prevent it from
getting fulfilled if we have the preimage stored. The preimage will not
be released before the the payment hash gets removed from
dont_settle_htlcs. Htlcs can still get expired as usual or failed if no
preimage is known.
This is only used by Just-in-time channel openings.

Registering a payment hash in dont_expire_htlcs allows to overwrite the
minimum final cltv delta value after which htlcs would usually get
expired. This allows to delay expiry of htlcs or, if the value in the
dont_settle_htlcs dict is None, completely prevent expiry and let the
htlc get expired onchain.

Splitting this up in two different dicts makes it more explicit and
easier to reason about what they are actually doing.

 Please enter the commit message for your changes. Lines starting
2025-11-27 17:58:44 +01:00
f321x 95729a08ef lnpeer: report htlc_switch exceptions to crash reporter
It seems useful to report exceptions happening in the htlc_switch to the
crash reporter as it shouldn't raise exceptions in theory and this could
help catch subtle bugs.
2025-11-27 17:58:41 +01:00
f321x 0f314d1dd9 lnpeer/lnworker: refactor htlc_switch
refactor `htlc_switch` to new architecture to make it more robust
against partial settlement of htlc sets and increase maintainability.
Htlcs are now processed in two steps, first the htlcs are collected into
sets from the channels, and potentially failed on their own already.
Then a second loop iterates over the htlc sets and finalizes only on
whole sets.

# Conflicts:
#	electrum/lnpeer.py
2025-11-27 17:57:14 +01:00
f321x 41d391a617 Fix: For inner trampoline onions amt_to_forward can be larger than the htlc amount
Add unittest to TestPeerForwarding which sends a multi trampoline
payment.

Wait another htlc_switch iteration in tests because trampolines might have different delays
2025-11-27 17:44:31 +01:00
f321x a6e103b63c lnonion:
add helper properties
 minor refactoring
2025-11-25 13:14:57 +01:00
f321x c65ec6b2b9 add helper method: run_htlc_switch_iteration
no code modification, indent-only
2025-11-25 13:14:46 +01:00
ThomasV 077bcf515d StoredDict: use pointers instead of path
Instead of storing its own path, each StoredDict element stores
its own key and a pointer to its parent. If a dict is removed
from the db, its parent pointer is set to None. This makes
self.path return None for all branches that have been pruned.

This passes tests/tests_json_db.py and fixes issue #10000
2025-11-07 10:00:10 +01:00
f321x a5cf5f75fc lnpeer: await init in main_loop
Because `LNPeer.initialized` was awaited in
`LNPeer._query_gossip()` instead of the main loop the other tasks got
spawned concurrently and each task on its own has to wait for the
initialization. In `LNPeer._send_own_gossip()` this was missing, instead
there is a fixed 10 sec sleep. If the connection was not initialized but
the 10 sec are exceeded `_send_own_gossip()` tries to send gossip and
causes this exception as the `LNTransport` is not ready:

```
  2.13 | E | lnpeer.Peer.[LNWallet, 0288fa27c0-bc1900c8] | Exception in main_loop: AttributeError("'LNTransport' object has no attribute 'sk'")
Traceback (most recent call last):
  File "/home/user/code/electrum-fork/electrum/util.py", line 1232, in wrapper
    return await func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnpeer.py", line 511, in wrapper_func
    return await func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnpeer.py", line 525, in main_loop
    async with self.taskgroup as group:
               ^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/env/lib/python3.14/site-packages/aiorpcx/curio.py", line 304, in __aexit__
    await self.join()
  File "/home/user/code/electrum-fork/electrum/util.py", line 1420, in join
    task.result()
    ~~~~~~~~~~~^^
  File "/home/user/code/electrum-fork/electrum/lnpeer.py", line 573, in _send_own_gossip
    self.send_node_announcement(alias, color)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lnpeer.py", line 1830, in send_node_announcement
    self.transport.send_bytes(raw_msg)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/home/user/code/electrum-fork/electrum/lntransport.py", line 225, in send_bytes
    lc = aead_encrypt(self.sk, self.sn(), b'', l)
                      ^^^^^^^
AttributeError: 'LNTransport' object has no attribute 'sk'. Did you mean: 'sn'?
```

By awaiting the initialization directly in the `main_loop` it is more
clear that the task getting spawned subsequently depend on the transport
being available and separates the initialization more clearly these
other functions.
2025-10-28 15:42:16 +01:00
f321x d62b627a0b lnpeer: move htlc forwarding funcs to lnworker
forwarding happens independent of the peer that received the htlc to
forward and fits better in lnworker.
2025-09-30 09:54:24 +02:00
f321x 32aa6ab20c lnutil: rename RecvMPPResolution.ACCEPTED
Renames RecvMPPResolution.ACCEPTED to .COMPLETE as .ACCEPTED is somewhat
misleading. Accepted could imply that the preimage for this set has been
revealed or that the set has been settled, however it only means that we
have received the full set (it is complete), but the set still can be
failed (e.g. through cltv timeout) and has not been claimed yet.
2025-09-29 16:11:26 +00:00
f321x 7d0a69a9ce lnpeer: only spawn htlc_switch for peers with LNWallet
stop spawning htlc_switch for LNGossip peers, they don't handle any
htlcs
2025-09-29 16:11:20 +00:00
f321x fcc3796079 lnworker: move RecvMPPResolution and status to lnutil
it is required both in lnpeer and lnworker, moving it to lnutil seems to
make more sense.

# Conflicts:
#	electrum/lnworker.py
2025-09-29 16:11:17 +00:00
f321x 9db975f9d7 lightning: remove legacy payment secret derivation
This seems old and not very useful anymore.
2025-09-29 16:11:13 +00:00
f321x acd52da764 lnpeer: cleanup imports 2025-09-29 16:10:59 +00:00