tlnchannel: update_fee: improve "can afford" check - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 996799d79ed737dcf8048edea452bcd608b3ada1
 (DIR) parent e59eb147c04572a313853e19b8c0e58416eb452f
 (HTM) Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon, 15 Jun 2020 15:42:56 +0200
       
       lnchannel: update_fee: improve "can afford" check
       
       Diffstat:
         M electrum/lnchannel.py               |      31 ++++++++++++++++++++-----------
         M electrum/lnutil.py                  |      16 ++++++++++++----
       
       2 files changed, 32 insertions(+), 15 deletions(-)
       ---
 (DIR) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -1108,7 +1108,8 @@ class Channel(AbstractChannel):
                return max_send_msat
        
        
       -    def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None) -> Sequence[UpdateAddHtlc]:
       +    def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None, *,
       +                       feerate: int = None) -> Sequence[UpdateAddHtlc]:
                """Returns list of non-dust HTLCs for subject's commitment tx at ctn,
                filtered by direction (of HTLCs).
                """
       t@@ -1116,7 +1117,8 @@ class Channel(AbstractChannel):
                assert type(direction) is Direction
                if ctn is None:
                    ctn = self.get_oldest_unrevoked_ctn(subject)
       -        feerate = self.get_feerate(subject, ctn=ctn)
       +        if feerate is None:
       +            feerate = self.get_feerate(subject, ctn=ctn)
                conf = self.config[subject]
                if direction == RECEIVED:
                    threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate)
       t@@ -1254,8 +1256,22 @@ class Channel(AbstractChannel):
                # feerate uses sat/kw
                if self.constraints.is_initiator != from_us:
                    raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
       -        # TODO check that funder can afford the new on-chain fees (+ channel reserve)
       -        #      (maybe check both ctxs, at least if from_us is True??)
       +        sender = LOCAL if from_us else REMOTE
       +        ctx_owner = -sender
       +        ctn = self.get_next_ctn(ctx_owner)
       +        sender_balance_msat = self.balance_minus_outgoing_htlcs(whose=sender, ctx_owner=ctx_owner, ctn=ctn)
       +        sender_reserve_msat = self.config[-sender].reserve_sat * 1000
       +        num_htlcs_in_ctx = len(self.included_htlcs(ctx_owner, SENT, ctn=ctn, feerate=feerate) +
       +                               self.included_htlcs(ctx_owner, RECEIVED, ctn=ctn, feerate=feerate))
       +        ctx_fees_msat = calc_fees_for_commitment_tx(
       +            num_htlcs=num_htlcs_in_ctx,
       +            feerate=feerate,
       +            is_local_initiator=self.constraints.is_initiator,
       +        )
       +        remainder = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]
       +        if remainder < 0:
       +            raise Exception(f"Cannot update_fee. {sender} tried to update fee but they cannot afford it. "
       +                            f"Their balance would go below reserve: {remainder} msat missing.")
                with self.db_lock:
                    if from_us:
                        assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
       t@@ -1302,13 +1318,6 @@ class Channel(AbstractChannel):
                    is_local_initiator=self.constraints.is_initiator == (subject == LOCAL),
                )
        
       -        # TODO: we need to also include the respective channel reserves here, but not at the
       -        #       beginning of the channel lifecycle when the reserve might not be met yet
       -        if remote_msat - onchain_fees[REMOTE] < 0:
       -            raise Exception(f"negative remote_msat in make_commitment: {remote_msat}")
       -        if local_msat - onchain_fees[LOCAL] < 0:
       -            raise Exception(f"negative local_msat in make_commitment: {local_msat}")
       -
                if self.is_static_remotekey_enabled():
                    payment_pubkey = other_config.payment_basepoint.pubkey
                else:
 (DIR) diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -639,8 +639,12 @@ class HTLCOwner(IntFlag):
            LOCAL = 1
            REMOTE = -LOCAL
        
       -    def inverted(self):
       -        return HTLCOwner(-self)
       +    def inverted(self) -> 'HTLCOwner':
       +        return -self
       +
       +    def __neg__(self) -> 'HTLCOwner':
       +        return HTLCOwner(super().__neg__())
       +
        
        class Direction(IntFlag):
            SENT = -1     # in the context of HTLCs: "offered" HTLCs
       t@@ -654,10 +658,14 @@ REMOTE = HTLCOwner.REMOTE
        
        def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
                local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
       -    to_local_amt = local_amount_msat - fees_per_participant[LOCAL]
       +    # BOLT-03: "Base commitment transaction fees are extracted from the funder's amount;
       +    #           if that amount is insufficient, the entire amount of the funder's output is used."
       +    #   -> if funder cannot afford feerate, their output might go negative, so take max(0, x) here:
       +    to_local_amt = max(0, local_amount_msat - fees_per_participant[LOCAL])
            to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000)
       -    to_remote_amt = remote_amount_msat - fees_per_participant[REMOTE]
       +    to_remote_amt = max(0, remote_amount_msat - fees_per_participant[REMOTE])
            to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000)
       +
            non_htlc_outputs = [to_local, to_remote]
            htlc_outputs = []
            for script, htlc in htlcs: