lnhtlc.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)