tlnhtlc.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tlnhtlc.py (29661B)
---
1 from copy import deepcopy
2 from typing import Optional, Sequence, Tuple, List, Dict, TYPE_CHECKING, Set
3 import threading
4
5 from .lnutil import SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, UpdateAddHtlc, Direction, FeeUpdate
6 from .util import bh2u, bfh
7
8 if TYPE_CHECKING:
9 from .json_db import StoredDict
10
11
12 class HTLCManager:
13
14 def __init__(self, log:'StoredDict', *, initial_feerate=None):
15
16 if len(log) == 0:
17 initial = {
18 'adds': {}, # "side who offered htlc" -> htlc_id -> htlc
19 'locked_in': {}, # "side who offered htlc" -> action -> htlc_id -> whose ctx -> ctn
20 'settles': {}, # "side who offered htlc" -> action -> htlc_id -> whose ctx -> ctn
21 'fails': {}, # "side who offered htlc" -> action -> htlc_id -> whose ctx -> ctn
22 'fee_updates': {}, # "side who initiated fee update" -> action -> list of FeeUpdates
23 'revack_pending': False,
24 'next_htlc_id': 0,
25 'ctn': -1, # oldest unrevoked ctx of sub
26 }
27 # note: "htlc_id" keys in dict are str! but due to json_db magic they can *almost* be treated as int...
28 log[LOCAL] = deepcopy(initial)
29 log[REMOTE] = deepcopy(initial)
30 log['unacked_local_updates2'] = {}
31
32 if 'unfulfilled_htlcs' not in log:
33 log['unfulfilled_htlcs'] = {} # htlc_id -> onion_packet
34 if 'fail_htlc_reasons' not in log:
35 log['fail_htlc_reasons'] = {} # htlc_id -> error_bytes, failure_message
36
37 # maybe bootstrap fee_updates if initial_feerate was provided
38 if initial_feerate is not None:
39 assert type(initial_feerate) is int
40 for sub in (LOCAL, REMOTE):
41 if not log[sub]['fee_updates']:
42 log[sub]['fee_updates'][0] = FeeUpdate(rate=initial_feerate, ctn_local=0, ctn_remote=0)
43 self.log = log
44
45 # We need a lock as many methods of HTLCManager are accessed by both the asyncio thread and the GUI.
46 # lnchannel sometimes calls us with Channel.db_lock (== log.lock) already taken,
47 # and we ourselves often take log.lock (via StoredDict.__getitem__).
48 # Hence, to avoid deadlocks, we reuse this same lock.
49 self.lock = log.lock
50
51 self._init_maybe_active_htlc_ids()
52
53 def with_lock(func):
54 def func_wrapper(self, *args, **kwargs):
55 with self.lock:
56 return func(self, *args, **kwargs)
57 return func_wrapper
58
59 @with_lock
60 def ctn_latest(self, sub: HTLCOwner) -> int:
61 """Return the ctn for the latest (newest that has a valid sig) ctx of sub"""
62 return self.ctn_oldest_unrevoked(sub) + int(self.is_revack_pending(sub))
63
64 def ctn_oldest_unrevoked(self, sub: HTLCOwner) -> int:
65 """Return the ctn for the oldest unrevoked ctx of sub"""
66 return self.log[sub]['ctn']
67
68 def is_revack_pending(self, sub: HTLCOwner) -> bool:
69 """Returns True iff sub was sent commitment_signed but they did not
70 send revoke_and_ack yet (sub has multiple unrevoked ctxs)
71 """
72 return self.log[sub]['revack_pending']
73
74 def _set_revack_pending(self, sub: HTLCOwner, pending: bool) -> None:
75 self.log[sub]['revack_pending'] = pending
76
77 def get_next_htlc_id(self, sub: HTLCOwner) -> int:
78 return self.log[sub]['next_htlc_id']
79
80 ##### Actions on channel:
81
82 @with_lock
83 def channel_open_finished(self):
84 self.log[LOCAL]['ctn'] = 0
85 self.log[REMOTE]['ctn'] = 0
86 self._set_revack_pending(LOCAL, False)
87 self._set_revack_pending(REMOTE, False)
88
89 @with_lock
90 def send_htlc(self, htlc: UpdateAddHtlc) -> UpdateAddHtlc:
91 htlc_id = htlc.htlc_id
92 if htlc_id != self.get_next_htlc_id(LOCAL):
93 raise Exception(f"unexpected local htlc_id. next should be "
94 f"{self.get_next_htlc_id(LOCAL)} but got {htlc_id}")
95 self.log[LOCAL]['adds'][htlc_id] = htlc
96 self.log[LOCAL]['locked_in'][htlc_id] = {LOCAL: None, REMOTE: self.ctn_latest(REMOTE)+1}
97 self.log[LOCAL]['next_htlc_id'] += 1
98 self._maybe_active_htlc_ids[LOCAL].add(htlc_id)
99 return htlc
100
101 @with_lock
102 def recv_htlc(self, htlc: UpdateAddHtlc) -> None:
103 htlc_id = htlc.htlc_id
104 if htlc_id != self.get_next_htlc_id(REMOTE):
105 raise Exception(f"unexpected remote htlc_id. next should be "
106 f"{self.get_next_htlc_id(REMOTE)} but got {htlc_id}")
107 self.log[REMOTE]['adds'][htlc_id] = htlc
108 self.log[REMOTE]['locked_in'][htlc_id] = {LOCAL: self.ctn_latest(LOCAL)+1, REMOTE: None}
109 self.log[REMOTE]['next_htlc_id'] += 1
110 self._maybe_active_htlc_ids[REMOTE].add(htlc_id)
111
112 @with_lock
113 def send_settle(self, htlc_id: int) -> None:
114 next_ctn = self.ctn_latest(REMOTE) + 1
115 if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):
116 raise Exception(f"(local) cannot remove htlc that is not there...")
117 self.log[REMOTE]['settles'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}
118
119 @with_lock
120 def recv_settle(self, htlc_id: int) -> None:
121 next_ctn = self.ctn_latest(LOCAL) + 1
122 if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):
123 raise Exception(f"(remote) cannot remove htlc that is not there...")
124 self.log[LOCAL]['settles'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}
125
126 @with_lock
127 def send_fail(self, htlc_id: int) -> None:
128 next_ctn = self.ctn_latest(REMOTE) + 1
129 if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):
130 raise Exception(f"(local) cannot remove htlc that is not there...")
131 self.log[REMOTE]['fails'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}
132
133 @with_lock
134 def recv_fail(self, htlc_id: int) -> None:
135 next_ctn = self.ctn_latest(LOCAL) + 1
136 if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):
137 raise Exception(f"(remote) cannot remove htlc that is not there...")
138 self.log[LOCAL]['fails'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}
139
140 @with_lock
141 def send_update_fee(self, feerate: int) -> None:
142 fee_update = FeeUpdate(rate=feerate,
143 ctn_local=None, ctn_remote=self.ctn_latest(REMOTE) + 1)
144 self._new_feeupdate(fee_update, subject=LOCAL)
145
146 @with_lock
147 def recv_update_fee(self, feerate: int) -> None:
148 fee_update = FeeUpdate(rate=feerate,
149 ctn_local=self.ctn_latest(LOCAL) + 1, ctn_remote=None)
150 self._new_feeupdate(fee_update, subject=REMOTE)
151
152 @with_lock
153 def _new_feeupdate(self, fee_update: FeeUpdate, subject: HTLCOwner) -> None:
154 # overwrite last fee update if not yet committed to by anyone; otherwise append
155 d = self.log[subject]['fee_updates']
156 #assert type(d) is StoredDict
157 n = len(d)
158 last_fee_update = d[n-1]
159 if (last_fee_update.ctn_local is None or last_fee_update.ctn_local > self.ctn_latest(LOCAL)) \
160 and (last_fee_update.ctn_remote is None or last_fee_update.ctn_remote > self.ctn_latest(REMOTE)):
161 d[n-1] = fee_update
162 else:
163 d[n] = fee_update
164
165 @with_lock
166 def send_ctx(self) -> None:
167 assert self.ctn_latest(REMOTE) == self.ctn_oldest_unrevoked(REMOTE), (self.ctn_latest(REMOTE), self.ctn_oldest_unrevoked(REMOTE))
168 self._set_revack_pending(REMOTE, True)
169
170 @with_lock
171 def recv_ctx(self) -> None:
172 assert self.ctn_latest(LOCAL) == self.ctn_oldest_unrevoked(LOCAL), (self.ctn_latest(LOCAL), self.ctn_oldest_unrevoked(LOCAL))
173 self._set_revack_pending(LOCAL, True)
174
175 @with_lock
176 def send_rev(self) -> None:
177 self.log[LOCAL]['ctn'] += 1
178 self._set_revack_pending(LOCAL, False)
179 # htlcs
180 for htlc_id in self._maybe_active_htlc_ids[REMOTE]:
181 ctns = self.log[REMOTE]['locked_in'][htlc_id]
182 if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):
183 ctns[REMOTE] = self.ctn_latest(REMOTE) + 1
184 for log_action in ('settles', 'fails'):
185 for htlc_id in self._maybe_active_htlc_ids[LOCAL]:
186 ctns = self.log[LOCAL][log_action].get(htlc_id, None)
187 if ctns is None: continue
188 if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):
189 ctns[REMOTE] = self.ctn_latest(REMOTE) + 1
190 self._update_maybe_active_htlc_ids()
191 # fee updates
192 for k, fee_update in list(self.log[REMOTE]['fee_updates'].items()):
193 if fee_update.ctn_remote is None and fee_update.ctn_local <= self.ctn_latest(LOCAL):
194 fee_update.ctn_remote = self.ctn_latest(REMOTE) + 1
195
196 @with_lock
197 def recv_rev(self) -> None:
198 self.log[REMOTE]['ctn'] += 1
199 self._set_revack_pending(REMOTE, False)
200 # htlcs
201 for htlc_id in self._maybe_active_htlc_ids[LOCAL]:
202 ctns = self.log[LOCAL]['locked_in'][htlc_id]
203 if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):
204 ctns[LOCAL] = self.ctn_latest(LOCAL) + 1
205 for log_action in ('settles', 'fails'):
206 for htlc_id in self._maybe_active_htlc_ids[REMOTE]:
207 ctns = self.log[REMOTE][log_action].get(htlc_id, None)
208 if ctns is None: continue
209 if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):
210 ctns[LOCAL] = self.ctn_latest(LOCAL) + 1
211 self._update_maybe_active_htlc_ids()
212 # fee updates
213 for k, fee_update in list(self.log[LOCAL]['fee_updates'].items()):
214 if fee_update.ctn_local is None and fee_update.ctn_remote <= self.ctn_latest(REMOTE):
215 fee_update.ctn_local = self.ctn_latest(LOCAL) + 1
216
217 # no need to keep local update raw msgs anymore, they have just been ACKed.
218 self.log['unacked_local_updates2'].pop(self.log[REMOTE]['ctn'], None)
219
220 @with_lock
221 def _update_maybe_active_htlc_ids(self) -> None:
222 # - Loosely, we want a set that contains the htlcs that are
223 # not "removed and revoked from all ctxs of both parties". (self._maybe_active_htlc_ids)
224 # It is guaranteed that those htlcs are in the set, but older htlcs might be there too:
225 # there is a sanity margin of 1 ctn -- this relaxes the care needed re order of method calls.
226 # - balance_delta is in sync with maybe_active_htlc_ids. When htlcs are removed from the latter,
227 # balance_delta is updated to reflect that htlc.
228 sanity_margin = 1
229 for htlc_proposer in (LOCAL, REMOTE):
230 for log_action in ('settles', 'fails'):
231 for htlc_id in list(self._maybe_active_htlc_ids[htlc_proposer]):
232 ctns = self.log[htlc_proposer][log_action].get(htlc_id, None)
233 if ctns is None: continue
234 if (ctns[LOCAL] is not None
235 and ctns[LOCAL] <= self.ctn_oldest_unrevoked(LOCAL) - sanity_margin
236 and ctns[REMOTE] is not None
237 and ctns[REMOTE] <= self.ctn_oldest_unrevoked(REMOTE) - sanity_margin):
238 self._maybe_active_htlc_ids[htlc_proposer].remove(htlc_id)
239 if log_action == 'settles':
240 htlc = self.log[htlc_proposer]['adds'][htlc_id] # type: UpdateAddHtlc
241 self._balance_delta -= htlc.amount_msat * htlc_proposer
242
243 @with_lock
244 def _init_maybe_active_htlc_ids(self):
245 # first idx is "side who offered htlc":
246 self._maybe_active_htlc_ids = {LOCAL: set(), REMOTE: set()} # type: Dict[HTLCOwner, Set[int]]
247 # add all htlcs
248 self._balance_delta = 0 # the balance delta of LOCAL since channel open
249 for htlc_proposer in (LOCAL, REMOTE):
250 for htlc_id in self.log[htlc_proposer]['adds']:
251 self._maybe_active_htlc_ids[htlc_proposer].add(htlc_id)
252 # remove old htlcs
253 self._update_maybe_active_htlc_ids()
254
255 @with_lock
256 def discard_unsigned_remote_updates(self):
257 """Discard updates sent by the remote, that the remote itself
258 did not yet sign (i.e. there was no corresponding commitment_signed msg)
259 """
260 # htlcs added
261 for htlc_id, ctns in list(self.log[REMOTE]['locked_in'].items()):
262 if ctns[LOCAL] > self.ctn_latest(LOCAL):
263 del self.log[REMOTE]['locked_in'][htlc_id]
264 del self.log[REMOTE]['adds'][htlc_id]
265 self._maybe_active_htlc_ids[REMOTE].discard(htlc_id)
266 if self.log[REMOTE]['locked_in']:
267 self.log[REMOTE]['next_htlc_id'] = max([int(x) for x in self.log[REMOTE]['locked_in'].keys()]) + 1
268 else:
269 self.log[REMOTE]['next_htlc_id'] = 0
270 # htlcs removed
271 for log_action in ('settles', 'fails'):
272 for htlc_id, ctns in list(self.log[LOCAL][log_action].items()):
273 if ctns[LOCAL] > self.ctn_latest(LOCAL):
274 del self.log[LOCAL][log_action][htlc_id]
275 # fee updates
276 for k, fee_update in list(self.log[REMOTE]['fee_updates'].items()):
277 if fee_update.ctn_local > self.ctn_latest(LOCAL):
278 self.log[REMOTE]['fee_updates'].pop(k)
279
280 @with_lock
281 def store_local_update_raw_msg(self, raw_update_msg: bytes, *, is_commitment_signed: bool) -> None:
282 """We need to be able to replay unacknowledged updates we sent to the remote
283 in case of disconnections. Hence, raw update and commitment_signed messages
284 are stored temporarily (until they are acked)."""
285 # self.log['unacked_local_updates2'][ctn_idx] is a list of raw messages
286 # containing some number of updates and then a single commitment_signed
287 if is_commitment_signed:
288 ctn_idx = self.ctn_latest(REMOTE)
289 else:
290 ctn_idx = self.ctn_latest(REMOTE) + 1
291 l = self.log['unacked_local_updates2'].get(ctn_idx, [])
292 l.append(raw_update_msg.hex())
293 self.log['unacked_local_updates2'][ctn_idx] = l
294
295 @with_lock
296 def get_unacked_local_updates(self) -> Dict[int, Sequence[bytes]]:
297 #return self.log['unacked_local_updates2']
298 return {int(ctn): [bfh(msg) for msg in messages]
299 for ctn, messages in self.log['unacked_local_updates2'].items()}
300
301 ##### Queries re HTLCs:
302
303 def get_htlc_by_id(self, htlc_proposer: HTLCOwner, htlc_id: int) -> UpdateAddHtlc:
304 return self.log[htlc_proposer]['adds'][htlc_id]
305
306 @with_lock
307 def is_htlc_active_at_ctn(self, *, ctx_owner: HTLCOwner, ctn: int,
308 htlc_proposer: HTLCOwner, htlc_id: int) -> bool:
309 htlc_id = int(htlc_id)
310 if htlc_id >= self.get_next_htlc_id(htlc_proposer):
311 return False
312 settles = self.log[htlc_proposer]['settles']
313 fails = self.log[htlc_proposer]['fails']
314 ctns = self.log[htlc_proposer]['locked_in'][htlc_id]
315 if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:
316 not_settled = htlc_id not in settles or settles[htlc_id][ctx_owner] is None or settles[htlc_id][ctx_owner] > ctn
317 not_failed = htlc_id not in fails or fails[htlc_id][ctx_owner] is None or fails[htlc_id][ctx_owner] > ctn
318 if not_settled and not_failed:
319 return True
320 return False
321
322 @with_lock
323 def is_htlc_irrevocably_added_yet(
324 self,
325 *,
326 ctx_owner: HTLCOwner = None,
327 htlc_proposer: HTLCOwner,
328 htlc_id: int,
329 ) -> bool:
330 """Returns whether `add_htlc` was irrevocably committed to `ctx_owner's` ctx.
331 If `ctx_owner` is None, both parties' ctxs are checked.
332 """
333 in_local = self._is_htlc_irrevocably_added_yet(
334 ctx_owner=LOCAL, htlc_proposer=htlc_proposer, htlc_id=htlc_id)
335 in_remote = self._is_htlc_irrevocably_added_yet(
336 ctx_owner=REMOTE, htlc_proposer=htlc_proposer, htlc_id=htlc_id)
337 if ctx_owner is None:
338 return in_local and in_remote
339 elif ctx_owner == LOCAL:
340 return in_local
341 elif ctx_owner == REMOTE:
342 return in_remote
343 else:
344 raise Exception(f"unexpected ctx_owner: {ctx_owner!r}")
345
346 @with_lock
347 def _is_htlc_irrevocably_added_yet(
348 self,
349 *,
350 ctx_owner: HTLCOwner,
351 htlc_proposer: HTLCOwner,
352 htlc_id: int,
353 ) -> bool:
354 htlc_id = int(htlc_id)
355 if htlc_id >= self.get_next_htlc_id(htlc_proposer):
356 return False
357 ctns = self.log[htlc_proposer]['locked_in'][htlc_id]
358 if ctns[ctx_owner] is None:
359 return False
360 return ctns[ctx_owner] <= self.ctn_oldest_unrevoked(ctx_owner)
361
362 @with_lock
363 def is_htlc_irrevocably_removed_yet(
364 self,
365 *,
366 ctx_owner: HTLCOwner = None,
367 htlc_proposer: HTLCOwner,
368 htlc_id: int,
369 ) -> bool:
370 """Returns whether the removal of an htlc was irrevocably committed to `ctx_owner's` ctx.
371 The removal can either be a fulfill/settle or a fail; they are not distinguished.
372 If `ctx_owner` is None, both parties' ctxs are checked.
373 """
374 in_local = self._is_htlc_irrevocably_removed_yet(
375 ctx_owner=LOCAL, htlc_proposer=htlc_proposer, htlc_id=htlc_id)
376 in_remote = self._is_htlc_irrevocably_removed_yet(
377 ctx_owner=REMOTE, htlc_proposer=htlc_proposer, htlc_id=htlc_id)
378 if ctx_owner is None:
379 return in_local and in_remote
380 elif ctx_owner == LOCAL:
381 return in_local
382 elif ctx_owner == REMOTE:
383 return in_remote
384 else:
385 raise Exception(f"unexpected ctx_owner: {ctx_owner!r}")
386
387 @with_lock
388 def _is_htlc_irrevocably_removed_yet(
389 self,
390 *,
391 ctx_owner: HTLCOwner,
392 htlc_proposer: HTLCOwner,
393 htlc_id: int,
394 ) -> bool:
395 htlc_id = int(htlc_id)
396 if htlc_id >= self.get_next_htlc_id(htlc_proposer):
397 return False
398 if htlc_id in self.log[htlc_proposer]['settles']:
399 ctn_of_settle = self.log[htlc_proposer]['settles'][htlc_id][ctx_owner]
400 else:
401 ctn_of_settle = None
402 if htlc_id in self.log[htlc_proposer]['fails']:
403 ctn_of_fail = self.log[htlc_proposer]['fails'][htlc_id][ctx_owner]
404 else:
405 ctn_of_fail = None
406 ctn_of_rm = ctn_of_settle or ctn_of_fail or None
407 if ctn_of_rm is None:
408 return False
409 return ctn_of_rm <= self.ctn_oldest_unrevoked(ctx_owner)
410
411 @with_lock
412 def htlcs_by_direction(self, subject: HTLCOwner, direction: Direction,
413 ctn: int = None) -> Dict[int, UpdateAddHtlc]:
414 """Return the dict of received or sent (depending on direction) HTLCs
415 in subject's ctx at ctn, keyed by htlc_id.
416
417 direction is relative to subject!
418 """
419 assert type(subject) is HTLCOwner
420 assert type(direction) is Direction
421 if ctn is None:
422 ctn = self.ctn_oldest_unrevoked(subject)
423 d = {}
424 # subject's ctx
425 # party is the proposer of the HTLCs
426 party = subject if direction == SENT else subject.inverted()
427 if ctn >= self.ctn_oldest_unrevoked(subject):
428 considered_htlc_ids = self._maybe_active_htlc_ids[party]
429 else: # ctn is too old; need to consider full log (slow...)
430 considered_htlc_ids = self.log[party]['locked_in']
431 for htlc_id in considered_htlc_ids:
432 htlc_id = int(htlc_id)
433 if self.is_htlc_active_at_ctn(ctx_owner=subject, ctn=ctn, htlc_proposer=party, htlc_id=htlc_id):
434 d[htlc_id] = self.log[party]['adds'][htlc_id]
435 return d
436
437 @with_lock
438 def htlcs(self, subject: HTLCOwner, ctn: int = None) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
439 """Return the list of HTLCs in subject's ctx at ctn."""
440 assert type(subject) is HTLCOwner
441 if ctn is None:
442 ctn = self.ctn_oldest_unrevoked(subject)
443 l = []
444 l += [(SENT, x) for x in self.htlcs_by_direction(subject, SENT, ctn).values()]
445 l += [(RECEIVED, x) for x in self.htlcs_by_direction(subject, RECEIVED, ctn).values()]
446 return l
447
448 @with_lock
449 def get_htlcs_in_oldest_unrevoked_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
450 assert type(subject) is HTLCOwner
451 ctn = self.ctn_oldest_unrevoked(subject)
452 return self.htlcs(subject, ctn)
453
454 @with_lock
455 def get_htlcs_in_latest_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
456 assert type(subject) is HTLCOwner
457 ctn = self.ctn_latest(subject)
458 return self.htlcs(subject, ctn)
459
460 @with_lock
461 def get_htlcs_in_next_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
462 assert type(subject) is HTLCOwner
463 ctn = self.ctn_latest(subject) + 1
464 return self.htlcs(subject, ctn)
465
466 def was_htlc_preimage_released(self, *, htlc_id: int, htlc_proposer: HTLCOwner) -> bool:
467 settles = self.log[htlc_proposer]['settles']
468 if htlc_id not in settles:
469 return False
470 return settles[htlc_id][htlc_proposer] is not None
471
472 def was_htlc_failed(self, *, htlc_id: int, htlc_proposer: HTLCOwner) -> bool:
473 """Returns whether an HTLC has been (or will be if we already know) failed."""
474 fails = self.log[htlc_proposer]['fails']
475 if htlc_id not in fails:
476 return False
477 return fails[htlc_id][htlc_proposer] is not None
478
479 @with_lock
480 def all_settled_htlcs_ever_by_direction(self, subject: HTLCOwner, direction: Direction,
481 ctn: int = None) -> Sequence[UpdateAddHtlc]:
482 """Return the list of all HTLCs that have been ever settled in subject's
483 ctx up to ctn, filtered to only "direction".
484 """
485 assert type(subject) is HTLCOwner
486 if ctn is None:
487 ctn = self.ctn_oldest_unrevoked(subject)
488 # subject's ctx
489 # party is the proposer of the HTLCs
490 party = subject if direction == SENT else subject.inverted()
491 d = []
492 for htlc_id, ctns in self.log[party]['settles'].items():
493 if ctns[subject] is not None and ctns[subject] <= ctn:
494 d.append(self.log[party]['adds'][htlc_id])
495 return d
496
497 @with_lock
498 def all_settled_htlcs_ever(self, subject: HTLCOwner, ctn: int = None) \
499 -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
500 """Return the list of all HTLCs that have been ever settled in subject's
501 ctx up to ctn.
502 """
503 assert type(subject) is HTLCOwner
504 if ctn is None:
505 ctn = self.ctn_oldest_unrevoked(subject)
506 sent = [(SENT, x) for x in self.all_settled_htlcs_ever_by_direction(subject, SENT, ctn)]
507 received = [(RECEIVED, x) for x in self.all_settled_htlcs_ever_by_direction(subject, RECEIVED, ctn)]
508 return sent + received
509
510 @with_lock
511 def all_htlcs_ever(self) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:
512 sent = [(SENT, htlc) for htlc in self.log[LOCAL]['adds'].values()]
513 received = [(RECEIVED, htlc) for htlc in self.log[REMOTE]['adds'].values()]
514 return sent + received
515
516 @with_lock
517 def get_balance_msat(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None,
518 initial_balance_msat: int) -> int:
519 """Returns the balance of 'whose' in 'ctx' at 'ctn'.
520 Only HTLCs that have been settled by that ctn are counted.
521 """
522 if ctn is None:
523 ctn = self.ctn_oldest_unrevoked(ctx_owner)
524 balance = initial_balance_msat
525 if ctn >= self.ctn_oldest_unrevoked(ctx_owner):
526 balance += self._balance_delta * whose
527 considered_sent_htlc_ids = self._maybe_active_htlc_ids[whose]
528 considered_recv_htlc_ids = self._maybe_active_htlc_ids[-whose]
529 else: # ctn is too old; need to consider full log (slow...)
530 considered_sent_htlc_ids = self.log[whose]['settles']
531 considered_recv_htlc_ids = self.log[-whose]['settles']
532 # sent htlcs
533 for htlc_id in considered_sent_htlc_ids:
534 ctns = self.log[whose]['settles'].get(htlc_id, None)
535 if ctns is None: continue
536 if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:
537 htlc = self.log[whose]['adds'][htlc_id]
538 balance -= htlc.amount_msat
539 # recv htlcs
540 for htlc_id in considered_recv_htlc_ids:
541 ctns = self.log[-whose]['settles'].get(htlc_id, None)
542 if ctns is None: continue
543 if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:
544 htlc = self.log[-whose]['adds'][htlc_id]
545 balance += htlc.amount_msat
546 return balance
547
548 @with_lock
549 def _get_htlcs_that_got_removed_exactly_at_ctn(
550 self, ctn: int, *, ctx_owner: HTLCOwner, htlc_proposer: HTLCOwner, log_action: str,
551 ) -> Sequence[UpdateAddHtlc]:
552 if ctn >= self.ctn_oldest_unrevoked(ctx_owner):
553 considered_htlc_ids = self._maybe_active_htlc_ids[htlc_proposer]
554 else: # ctn is too old; need to consider full log (slow...)
555 considered_htlc_ids = self.log[htlc_proposer][log_action]
556 htlcs = []
557 for htlc_id in considered_htlc_ids:
558 ctns = self.log[htlc_proposer][log_action].get(htlc_id, None)
559 if ctns is None: continue
560 if ctns[ctx_owner] == ctn:
561 htlcs.append(self.log[htlc_proposer]['adds'][htlc_id])
562 return htlcs
563
564 def received_in_ctn(self, local_ctn: int) -> Sequence[UpdateAddHtlc]:
565 """
566 received htlcs that became fulfilled when we send a revocation.
567 we check only local, because they are committed in the remote ctx first.
568 """
569 return self._get_htlcs_that_got_removed_exactly_at_ctn(local_ctn,
570 ctx_owner=LOCAL,
571 htlc_proposer=REMOTE,
572 log_action='settles')
573
574 def sent_in_ctn(self, remote_ctn: int) -> Sequence[UpdateAddHtlc]:
575 """
576 sent htlcs that became fulfilled when we received a revocation
577 we check only remote, because they are committed in the local ctx first.
578 """
579 return self._get_htlcs_that_got_removed_exactly_at_ctn(remote_ctn,
580 ctx_owner=REMOTE,
581 htlc_proposer=LOCAL,
582 log_action='settles')
583
584 def failed_in_ctn(self, remote_ctn: int) -> Sequence[UpdateAddHtlc]:
585 """
586 sent htlcs that became failed when we received a revocation
587 we check only remote, because they are committed in the local ctx first.
588 """
589 return self._get_htlcs_that_got_removed_exactly_at_ctn(remote_ctn,
590 ctx_owner=REMOTE,
591 htlc_proposer=LOCAL,
592 log_action='fails')
593
594 ##### Queries re Fees:
595 # note: feerates are in sat/kw everywhere in this file
596
597 @with_lock
598 def get_feerate(self, subject: HTLCOwner, ctn: int) -> int:
599 """Return feerate (sat/kw) used in subject's commitment txn at ctn."""
600 ctn = max(0, ctn) # FIXME rm this
601 # only one party can update fees; use length of logs to figure out which:
602 assert not (len(self.log[LOCAL]['fee_updates']) > 1 and len(self.log[REMOTE]['fee_updates']) > 1)
603 fee_log = self.log[LOCAL]['fee_updates'] # type: Sequence[FeeUpdate]
604 if len(self.log[REMOTE]['fee_updates']) > 1:
605 fee_log = self.log[REMOTE]['fee_updates']
606 # binary search
607 left = 0
608 right = len(fee_log)
609 while True:
610 i = (left + right) // 2
611 ctn_at_i = fee_log[i].ctn_local if subject==LOCAL else fee_log[i].ctn_remote
612 if right - left <= 1:
613 break
614 if ctn_at_i is None: # Nones can only be on the right end
615 right = i
616 continue
617 if ctn_at_i <= ctn: # among equals, we want the rightmost
618 left = i
619 else:
620 right = i
621 assert ctn_at_i <= ctn
622 return fee_log[i].rate
623
624 def get_feerate_in_oldest_unrevoked_ctx(self, subject: HTLCOwner) -> int:
625 return self.get_feerate(subject=subject, ctn=self.ctn_oldest_unrevoked(subject))
626
627 def get_feerate_in_latest_ctx(self, subject: HTLCOwner) -> int:
628 return self.get_feerate(subject=subject, ctn=self.ctn_latest(subject))
629
630 def get_feerate_in_next_ctx(self, subject: HTLCOwner) -> int:
631 return self.get_feerate(subject=subject, ctn=self.ctn_latest(subject) + 1)