timplement data_loss_protect - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 944e4f0ba0b6bbfb64c802a2b80515b27ded6713 DIR parent fdf8d8609b632b6a3bf4202656e528bcf375ad2c HTML Author: SomberNight <somber.night@protonmail.com> Date: Fri, 2 Aug 2019 18:04:13 +0200 implement data_loss_protect so that we can spend their_ctx_to_remote even when we lost our state but have an old backup Diffstat: M electrum/lnchannel.py | 13 +++++++++---- M electrum/lnpeer.py | 38 +++++++++++++++++++++++++++---- M electrum/lnsweep.py | 5 ++++- 3 files changed, 46 insertions(+), 10 deletions(-) --- DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py t@@ -65,6 +65,8 @@ class ChannelJsonEncoder(json.JSONEncoder): RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) +class RemoteCtnTooFarInFuture(Exception): pass + def decodeAll(d, local): for k, v in d.items(): t@@ -85,10 +87,10 @@ def htlcsum(htlcs): # following two functions are used because json # doesn't store int keys and byte string values -def str_bytes_dict_from_save(x): +def str_bytes_dict_from_save(x) -> Dict[int, bytes]: return {int(k): bfh(v) for k,v in x.items()} -def str_bytes_dict_to_save(x): +def str_bytes_dict_to_save(x) -> Dict[str, str]: return {str(k): bh2u(v) for k, v in x.items()} t@@ -132,6 +134,7 @@ class Channel(Logger): self.short_channel_id_predicted = self.short_channel_id self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {})) self.force_closed = state.get('force_closed') + self.data_loss_protect_remote_pcp = str_bytes_dict_from_save(state.get('data_loss_protect_remote_pcp', {})) log = state.get('log') self.hm = HTLCManager(local_ctn=self.config[LOCAL].ctn, t@@ -507,11 +510,12 @@ class Channel(Logger): fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) return list(filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs)) - def get_secret_and_point(self, subject, ctn): + def get_secret_and_point(self, subject, ctn) -> Tuple[Optional[bytes], bytes]: assert type(subject) is HTLCOwner offset = ctn - self.get_current_ctn(subject) if subject == REMOTE: - assert offset <= 1, offset + if offset > 1: + raise RemoteCtnTooFarInFuture(f"offset: {offset}") conf = self.config[REMOTE] if offset == 1: secret = None t@@ -621,6 +625,7 @@ class Channel(Logger): "log": self.hm.to_save(), "onion_keys": str_bytes_dict_to_save(self.onion_keys), "force_closed": self.force_closed, + "data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp), } return to_save DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -30,7 +30,7 @@ from .logging import Logger from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, ProcessedOnionPacket) -from .lnchannel import Channel, RevokeAndAck, htlcsum +from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, funding_output_script, get_per_commitment_secret_from_seed, t@@ -720,7 +720,8 @@ class Peer(Logger): latest_remote_ctn = chan.hm.ctn_latest(REMOTE) next_remote_ctn = latest_remote_ctn + 1 # send message - if self.localfeatures & LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT: + dlp_enabled = self.localfeatures & LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT + if dlp_enabled: if oldest_unrevoked_remote_ctn == 0: last_rev_secret = 0 else: t@@ -746,6 +747,8 @@ class Peer(Logger): chan.set_state('OPENING') their_next_local_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big') their_oldest_unrevoked_remote_ctn = int.from_bytes(channel_reestablish_msg["next_remote_revocation_number"], 'big') + their_local_pcp = channel_reestablish_msg.get("my_current_per_commitment_point") + their_claim_of_our_last_per_commitment_secret = channel_reestablish_msg.get("your_last_per_commitment_secret") should_close_we_are_ahead = False should_close_they_are_ahead = False t@@ -786,9 +789,34 @@ class Peer(Logger): should_close_we_are_ahead = True else: should_close_they_are_ahead = True - - # TODO option_data_loss_protect - + # option_data_loss_protect + def are_datalossprotect_fields_valid() -> bool: + if their_local_pcp is None or their_claim_of_our_last_per_commitment_secret is None: + # if DLP was enabled, absence of fields is not OK + return not dlp_enabled + our_pcs, __ = chan.get_secret_and_point(LOCAL, their_oldest_unrevoked_remote_ctn - 1) + if our_pcs != their_claim_of_our_last_per_commitment_secret: + self.logger.error(f"channel_reestablish: (DLP) local PCS mismatch: {bh2u(our_pcs)} != {bh2u(their_claim_of_our_last_per_commitment_secret)}") + return False + try: + __, our_remote_pcp = chan.get_secret_and_point(REMOTE, their_next_local_ctn - 1) + except RemoteCtnTooFarInFuture: + pass + else: + if our_remote_pcp != their_local_pcp: + self.logger.error(f"channel_reestablish: (DLP) remote PCP mismatch: {bh2u(our_remote_pcp)} != {bh2u(their_local_pcp)}") + return False + return True + + if not are_datalossprotect_fields_valid(): + self.logger.error(f"channel_reestablish: data loss protect fields invalid.") + # TODO should we force-close? + return + else: + if dlp_enabled and should_close_they_are_ahead: + self.logger.warning(f"channel_reestablish: remote is ahead of us! luckily DLP is enabled. remote PCP: {bh2u(their_local_pcp)}") + chan.data_loss_protect_remote_pcp[their_next_local_ctn - 1] = their_local_pcp + self.lnworker.save_channel(chan) if should_close_they_are_ahead: self.logger.warning(f"channel_reestablish: remote is ahead of us! trying to get them to force-close.") self.try_to_get_remote_to_force_close_with_their_latest(chan_id) DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py t@@ -245,7 +245,7 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int, create_txns_for_htlc(htlc, is_received_htlc=True) return txs -def analyze_ctx(chan, ctx): +def analyze_ctx(chan: 'Channel', ctx: Transaction): # note: the remote sometimes has two valid non-revoked commitment transactions, # either of which could be broadcast (their_conf.ctn, their_conf.ctn+1) our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) t@@ -265,6 +265,9 @@ def analyze_ctx(chan, ctx): their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) is_revocation = True #_logger.info(f'tx for revoked: {list(txs.keys())}') + elif ctn in chan.data_loss_protect_remote_pcp: + their_pcp = chan.data_loss_protect_remote_pcp[ctn] + is_revocation = False else: return return ctn, their_pcp, is_revocation, per_commitment_secret