lnsweep: simplify maybe_reveal_preimage_for_htlc#10442
lnsweep: simplify maybe_reveal_preimage_for_htlc#10442SomberNight wants to merge 2 commits intospesmilo:masterfrom
Conversation
electrum/lnsweep.py
Outdated
| # - if this is an incoming payment, where | ||
| # - all the HTLCs have already been received (covering invoice amt) and are pending currently, and | ||
| # - no one knows the preimage besides us, and | ||
| # - this channel was just force-closed by either party | ||
| # -> then we can freely decide whether to claim the HTLCs or let them time out. | ||
| # Both choices are valid and safe. *Here* it is simpler to implement | ||
| # letting the HTLC time out, so that's what we do. |
There was a problem hiding this comment.
note: several of the regtest tests expect the code to make the other choice, as that's what we've been doing so far
There was a problem hiding this comment.
Besides the simplicity aspect you mention i can also think of these tradeoffs:
Claiming may consume a large amount of the htlcs value, esp during high mining fees, so it might be preferable to retry another lightning payment to pay less fees and let the sender pay the mining fees to time out the htlcs.
OTOH not claiming them would cause the payment to be stuck until the htlcs timed out, which is also quite bad experience for both sides of the payment.
Though in practice i guess it should not happen often, so simplicity might outweigh the cost of any potential UX improvement that gets triggered only rarely.
There was a problem hiding this comment.
note: decided not to change this behaviour for now
electrum/lnsweep.py
Outdated
| # - if this is an incoming payment, where | ||
| # - all the HTLCs have already been received (covering invoice amt) and are pending currently, and | ||
| # - no one knows the preimage besides us, and | ||
| # - this channel was just force-closed by either party | ||
| # -> then we can freely decide whether to claim the HTLCs or let them time out. | ||
| # Both choices are valid and safe. *Here* it is simpler to implement | ||
| # letting the HTLC time out, so that's what we do. |
There was a problem hiding this comment.
if we are forwarding, and the outgoing HTLC has already been irrevocably committed, we need to keep watching this incoming HTLC until cltv_abs and be prepared to still fulfill it if we learn the preimage from downstream
There was a problem hiding this comment.
note: this is currently not handled and we might lose money when forwarding.
electrum/electrum/lnwatcher.py
Lines 163 to 168 in 4f7b6e8
If
chan.get_ctx_sweep_info does not include an item for the incoming HTLC, e.g. because we don't YET know the preimage for it, lnwatcher might stop watching the channel and never redeem the incoming HTLC onchain, even after the outgoing HTLC gets fulfilled.
There was a problem hiding this comment.
This looks like another use case for KeepWatchingTXO, 7e1fd00
|
the concept looks good to me, I would prefer a db upgrade. |
a771758 to
b2064e6
Compare
b2064e6 to
a277589
Compare
d0eb87d to
0a08169
Compare
|
I squashed and force-pushed. There is a copy of the old branch here (commit). |
"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>
0a08169 to
0b2c7a8
Compare
| def save_preimage( | ||
| self, | ||
| payment_hash: bytes, | ||
| preimage: bytes, | ||
| *, | ||
| write_to_disk: bool = True, | ||
| mark_as_public: Optional[bool] = None, # see is_preimage_public | ||
| ): | ||
| assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}" | ||
| assert isinstance(preimage, bytes), f"expected bytes, but got {type(preimage)}" | ||
| if sha256(preimage) != payment_hash: | ||
| raise Exception("tried to save incorrect preimage for payment_hash") | ||
| if self._preimages.get(payment_hash.hex()) is not None: | ||
| return # we already have this preimage | ||
| self.logger.debug(f"saving preimage for {payment_hash.hex()}") | ||
| self._preimages[payment_hash.hex()] = preimage.hex() | ||
| old_tuple = _, old_is_public = self._preimages.get(payment_hash.hex(), (None, None)) | ||
| if mark_as_public is None: # if unset, keep current DB value | ||
| mark_as_public = old_is_public or False | ||
| if old_is_public and not mark_as_public: | ||
| raise Exception("preimage mark_as_public: True->False transition is forbidden") | ||
| # sanity checks and conversions done. | ||
| new_tuple = preimage.hex(), mark_as_public | ||
| if old_tuple == new_tuple: # no change | ||
| return | ||
| self.logger.debug(f"saving preimage for {payment_hash.hex()} (public={mark_as_public})") | ||
| self._preimages[payment_hash.hex()] = new_tuple |
There was a problem hiding this comment.
also, lnworker.save_preimage could take an additional parameter for this boolean.
I changed to this, as the previous API was too error-prone - there was already a bug in this branch due to that. (ordering of calls to lnworker.save_preimage vs lnworker.mark_preimage_as_public mattered)
Unfortunately, to keep the API (for callers) simple, I had to put quite a bit of complexity inside the implementation of save_preimage. Still, I think it's ~okay.
There was a problem hiding this comment.
regarding complexity, why does save_preimage require the payment_hash as parameter?
I guess we could kill that parameter, although the caller would probably want to check the hash in some cases.
| ) -> Tuple[Optional[bytes], Optional[KeepWatchingTXO]]: | ||
| """Given a Remote-added-HTLC, return the preimage if it's okay to reveal it on-chain.""" | ||
| if not chan.lnworker.is_complete_mpp(htlc.payment_hash): | ||
| """Given a Remote-added-HTLC, return the preimage if it's okay to reveal it on-chain. |
There was a problem hiding this comment.
lnsweep desperately needs unit tests. I have a WIP branch to build the architecture for it but we can merge this before I get to finish that.
There was a problem hiding this comment.
I have tested the current branch using regtest (using the 'timebomb' approach).
It behaves well.
"When should we reveal preimages onchain?"
This commit tries to simplify the thinking to:
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
we can't restrict revealing preimages to only already public ones both onchain and offchain: there needs to be a state transition out of non-public for our own invoices
then onchain we can simplify: enforce preimage already being publicin lnsweep._maybe_reveal_preimage_for_htlc: