diff --git a/hosts/by-name/servo/services/cryptocurrencies/clightning-sane/clightning-sane b/hosts/by-name/servo/services/cryptocurrencies/clightning-sane/clightning-sane index 085de4b64..9ba3f5aad 100755 --- a/hosts/by-name/servo/services/cryptocurrencies/clightning-sane/clightning-sane +++ b/hosts/by-name/servo/services/cryptocurrencies/clightning-sane/clightning-sane @@ -37,43 +37,51 @@ class TxBounds: min_msat: int max_msat: int + def __repr__(self) -> str: + return f"TxBounds({self.min_msat} <= msat <= {self.max_msat})" + def is_satisfiable(self) -> bool: return self.min_msat <= self.max_msat - def restrict_to_htlc(self, ch: "LocalChannel") -> "Self": + def restrict_to_htlc(self, ch: "LocalChannel", why: str = "") -> "Self": """ apply min/max HTLC size restrictions of the given channel. """ + if ch: + why = why or ch.scid + if why: why = f"{why}: " + new_min, new_max = self.min_msat, self.max_msat - if ch.htlc_minimum_msat > self.min_msat: - new_min = ch.htlc_minimum_msat - logger.debug(f"raising min_msat due to HTLC requirements: {self.min_msat} -> {new_min}") - if ch.htlc_maximum_msat < self.max_msat: - new_max = ch.htlc_maximum_msat - logger.debug(f"lowering max_msat due to HTLC requirements: {self.max_msat} -> {new_max}") + if ch.htlc_minimum > self.min_msat: + new_min = ch.htlc_minimum + logger.debug(f"{why}raising min_msat due to HTLC requirements: {self.min_msat} -> {new_min}") + if ch.htlc_maximum < self.max_msat: + new_max = ch.htlc_maximum + logger.debug(f"{why}lowering max_msat due to HTLC requirements: {self.max_msat} -> {new_max}") return TxBounds(min_msat=new_min, max_msat=new_max) - def restrict_to_zero_fees(self, ch: "LocalChannel"=None, base: int=0, ppm: int=0) -> "Self": + def restrict_to_zero_fees(self, ch: "LocalChannel"=None, base: int=0, ppm: int=0, why:str = "") -> "Self": """ restrict tx size such that PPM fees are zero. if the channel has a base fee, then `max_msat` is forced to 0. """ if ch: - self = self.restrict_to_zero_fees(base=ch.to_me["base_fee_millisatoshi"], ppm=ch.to_me["fee_per_millionth"]) + why = why or ch.directed_scid_to_me + self = self.restrict_to_zero_fees(base=ch.to_me["base_fee_millisatoshi"], ppm=ch.to_me["fee_per_millionth"], why=why) + + if why: why = f"{why}: " new_max = self.max_msat - if ppm != 0: - new_max = math.ceil(1000000 / ppm) - 1 - if new_max < self.max_msat: - logger.debug(f"decreasing max_msat due to fee ppm: {self.max_msat} -> {new_max}") + ppm_max = math.ceil(1000000 / ppm) - 1 if ppm != 0 else new_max + if ppm_max < new_max: + logger.debug(f"{why}decreasing max_msat due to fee ppm: {new_max} -> {ppm_max}") + new_max = ppm_max + if base != 0: - logger.debug("free route impossible: channel has base fees") + logger.debug(f"{why}free route impossible: channel has base fees") new_max = 0 - return TxBounds( - min_msat = self.min_msat, - max_msat = new_max, - ) + return TxBounds(min_msat=self.min_msat, max_msat=new_max) class LocalChannel: @@ -102,15 +110,17 @@ class LocalChannel: self.peer_ch = rpc.peerchannel(self.scid, self.remote_peer) def __repr__(self) -> str: - return self.to_str(with_scid=True, with_bal_ratio=True, with_cost=True) + return self.to_str(with_scid=True, with_bal_ratio=True, with_cost=True, with_ppm=True) - def to_str(self, with_scid: bool=False, with_bal_msat: bool=False, with_bal_ratio: bool=False, with_cost:bool = False) -> str: + def to_str(self, with_scid: bool=False, with_bal_msat: bool=False, with_bal_ratio: bool=False, with_cost:bool = False, with_ppm:bool = False) -> str: alias = f"({self.remote_alias})" scid = f" scid:{self.scid:>13}" if with_scid else "" bal = f" S:{int(self.sendable):11}/R:{int(self.receivable):11}" if with_bal_msat else "" ratio = f" MINE:{(100*self.send_ratio):>7.3f}%" if with_bal_ratio else "" cost = f" COST:{self.opportunity_cost_lent:>11}" if with_cost else "" - return f"channel{alias:30}{scid}{bal}{ratio}{cost}" + ppm_to_me = self.to_me["fee_per_millionth"] if self.to_me else "N/A" + ppm = f" THEIR_PPM:{ppm_to_me:>6}" if with_ppm else "" + return f"channel{alias:30}{scid}{bal}{ratio}{cost}{ppm}" @property @@ -136,12 +146,28 @@ class LocalChannel: return self.to_me["short_channel_id"] @property - def htlc_minimum_msat(self) -> Millisatoshi: - return max(self.from_me["htlc_minimum_msat"], self.to_me["htlc_minimum_msat"]) + def htlc_minimum_to_me(self) -> Millisatoshi: + return self.to_me["htlc_minimum_msat"] @property - def htlc_maximum_msat(self) -> Millisatoshi: - return min(self.from_me["htlc_maximum_msat"], self.to_me["htlc_maximum_msat"]) + def htlc_minimum_from_me(self) -> Millisatoshi: + return self.from_me["htlc_minimum_msat"] + + @property + def htlc_minimum(self) -> Millisatoshi: + return max(self.htlc_minimum_to_me, self.htlc_minimum_from_me) + + @property + def htlc_maximum_to_me(self) -> Millisatoshi: + return self.to_me["htlc_maximum_msat"] + + @property + def htlc_maximum_from_me(self) -> Millisatoshi: + return self.from_me["htlc_maximum_msat"] + + @property + def htlc_maximum(self) -> Millisatoshi: + return min(self.htlc_maximum_to_me, self.htlc_maximum_from_me) @property def direction_to_me(self) -> int: @@ -242,12 +268,13 @@ class Balancer: logger.info(f"rebalance {out_scid} -> {in_scid} failed in our own channel") return RebalanceResult.FAIL_PERMANENT - bounds = bounds.restrict_to_htlc(out_ch) + # bounds = bounds.restrict_to_htlc(out_ch) # htlc bounds seem to be enforced only in the outward direction bounds = bounds.restrict_to_htlc(in_ch) bounds = bounds.restrict_to_zero_fees(in_ch) if not bounds.is_satisfiable(): return RebalanceResult.FAIL_PERMANENT # no valid bounds + logger.debug(f"route with bounds {bounds}") route = self.route(out_ch, in_ch, bounds) logger.debug(f"route: {route}") if route == RouteError.NO_ROUTE: @@ -316,18 +343,17 @@ class Balancer: if send_msat != Millisatoshi(bounds.max_msat): logger.debug(f"found route with non-zero fee: {send_msat} -> {bounds.max_msat}. {route}") + error = None for hop in route: hop_scid = hop["channel"] hop_dir = hop["direction"] ch = self._get_directed_scid(hop_scid, hop_dir) if ch["base_fee_millisatoshi"] != 0: self.nonzero_base_channels.append(f"{hop_scid}/{hop_dir}") + error = RouteError.HAS_BASE_FEE bounds = bounds.restrict_to_zero_fees(ppm=ch["fee_per_millionth"]) - if any(hop["base_fee_millisatoshi"] != 0 for hop in route): - return RouteError.HAS_BASE_FEE - - return bounds + return bounds if error is None else error return route