backend support for 'close reason'#10578
Conversation
qt frontent support for 'close reason' fix test
SomberNight
left a comment
There was a problem hiding this comment.
Hey. Thanks for the PR. Added some comments.
| if who_closed == LOCAL: | ||
| self.logger.info(f'we (local) force closed') | ||
| elif who_closed == REMOTE: | ||
| self.logger.info(f'they (remote) force closed.') | ||
| self.save_close_reason(ChanCloseReason.REMOTE_FORCE) |
There was a problem hiding this comment.
You should also set the close reason in the LOCAL case here.
First of all, it is nicer to make the code (in the local scope) symmetric/homogeneous.
But also, ultimately the final close reason should reflect what gets mined and weird chains of events could happen: e.g. local force-close gets broadcast to mempool first, then remote force-close replaces it in mempool, but then local force-close gets mined. If you call save_close_reason here, it will self-heal.
We could keep the other call in lnworker._force_close_channel in addition to this one, as that one runs much earlier.
| LOCAL_BALANCE = enum.auto() | ||
| REMOTE_BALANCE = enum.auto() | ||
| CHANNEL_STATUS = enum.auto() | ||
| CLOSE_REASON = enum.auto() |
There was a problem hiding this comment.
I think it does not make sense to add a column for this. It takes a lot of horizontal space and adds noise.
Isn't it enough to show this in the channel details dialog?
There was a problem hiding this comment.
Agree. I only added because I thought someone might want to search for channels closed with a given reason. But if that's the case, we can fix it later. I will remove.
| 'remote_reserve': chan.config[LOCAL].reserve_sat, | ||
| 'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000, | ||
| 'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000, | ||
| 'close_reason': (r := chan.get_close_reason()) and r.name or None, # Harder to read, but keeps it within a single line. |
There was a problem hiding this comment.
note: this would make the names of the enum members part of the external API
also see other comment re wallet db upgrades
There was a problem hiding this comment.
re the comment # Harder to read, but keeps it within a single line. -- ok, but the comment itself is not needed IMO
| _who_closed: Optional[int] = None # HTLCOwner (1 or -1). 0 means "unknown" | ||
|
|
||
| def save_close_reason(self, reason: ChanCloseReason) -> None: | ||
| self.storage['close_reason'] = reason.name |
There was a problem hiding this comment.
This would make the names of the enum members part of the wallet db. That is, if someone later renames e.g. ChanCloseReason.LOCAL_FORCE to ChanCloseReason.LOCAL_FORCE_CLOSE, they need to do a wallet db upgrade.
IMO it is preferable to use the names and not the int values in the wallet db, so this is fine, but it is completely unintuitive and error-prone for the programmer, so at least a comment must be added.
Just duplicate this comment:
electrum/electrum/lnchannel.py
Lines 77 to 78 in 1235b4a
| await util.wait_for2(p2.initialized, 1) | ||
| await p1.close_channel(alice_channel.channel_id) | ||
| # alice's side is completed, but bob need to wait to see the reason. | ||
| await asyncio.sleep(1) # FIXME: use a better wait |
There was a problem hiding this comment.
On my machine, the test passes even with this sleep completely removed. Is that not the case for you?
Anyway, the simplest less-wasteful thing that could be done is:
async with util.async_timeout(1):
while not bob_channel.is_closed_or_closing():
await asyncio.sleep(0.01)Something more complicated:
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index a4950ba9d5..b65f78a041 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -1636,12 +1636,21 @@ class TestPeerDirect(TestPeer):
w2.network.config.TEST_SHUTDOWN_FEE = 100
w1.network.config.TEST_SHUTDOWN_LEGACY = True
w2.network.config.TEST_SHUTDOWN_LEGACY = True
+
+ any_chan_changed = asyncio.Event()
+ async def on_chan_changed(*args):
+ any_chan_changed.set()
+ any_chan_changed.clear()
+ util.register_callback(on_chan_changed, ["channel"])
+
async def action():
await util.wait_for2(p1.initialized, 1)
await util.wait_for2(p2.initialized, 1)
await p1.close_channel(alice_channel.channel_id)
# alice's side is completed, but bob need to wait to see the reason.
- await asyncio.sleep(1) # FIXME: use a better wait
+ async with util.async_timeout(1):
+ while not bob_channel.is_closed_or_closing():
+ await any_chan_changed.wait()
self.assertEqual(alice_channel.get_close_reason(), ChanCloseReason.LOCAL_COOP)
self.assertEqual(bob_channel.get_close_reason(), ChanCloseReason.REMOTE_COOP)
gath.cancel()
There was a problem hiding this comment.
Indeed it does, but that's will not be necessarily true for all runs.
I will use the simple wait, but thanks for sharing the more complete version.
Closes #10389 following @SomberNight 's suggestion. Used
reasoninstead ofdiagnosis, though.In the test, bob's close reason isn't guaranteed to be set by the time alice's close_channel returns, so I used a short sleep; please advise if there is a better way.