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: