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