electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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)