Browse Source

implement data_loss_protect

so that we can spend their_ctx_to_remote even when we lost our state
but have an old backup
master
SomberNight 6 years ago committed by ThomasV
parent
commit
944e4f0ba0
  1. 13
      electrum/lnchannel.py
  2. 38
      electrum/lnpeer.py
  3. 5
      electrum/lnsweep.py

13
electrum/lnchannel.py

@ -65,6 +65,8 @@ class ChannelJsonEncoder(json.JSONEncoder):
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
class RemoteCtnTooFarInFuture(Exception): pass
def decodeAll(d, local): def decodeAll(d, local):
for k, v in d.items(): for k, v in d.items():
@ -85,10 +87,10 @@ def htlcsum(htlcs):
# following two functions are used because json # following two functions are used because json
# doesn't store int keys and byte string values # 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()} 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()} return {str(k): bh2u(v) for k, v in x.items()}
@ -132,6 +134,7 @@ class Channel(Logger):
self.short_channel_id_predicted = self.short_channel_id self.short_channel_id_predicted = self.short_channel_id
self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {})) self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {}))
self.force_closed = state.get('force_closed') 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') log = state.get('log')
self.hm = HTLCManager(local_ctn=self.config[LOCAL].ctn, self.hm = HTLCManager(local_ctn=self.config[LOCAL].ctn,
@ -507,11 +510,12 @@ class Channel(Logger):
fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) 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)) 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 assert type(subject) is HTLCOwner
offset = ctn - self.get_current_ctn(subject) offset = ctn - self.get_current_ctn(subject)
if subject == REMOTE: if subject == REMOTE:
assert offset <= 1, offset if offset > 1:
raise RemoteCtnTooFarInFuture(f"offset: {offset}")
conf = self.config[REMOTE] conf = self.config[REMOTE]
if offset == 1: if offset == 1:
secret = None secret = None
@ -621,6 +625,7 @@ class Channel(Logger):
"log": self.hm.to_save(), "log": self.hm.to_save(),
"onion_keys": str_bytes_dict_to_save(self.onion_keys), "onion_keys": str_bytes_dict_to_save(self.onion_keys),
"force_closed": self.force_closed, "force_closed": self.force_closed,
"data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp),
} }
return to_save return to_save

38
electrum/lnpeer.py

@ -30,7 +30,7 @@ from .logging import Logger
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
ProcessedOnionPacket) ProcessedOnionPacket)
from .lnchannel import Channel, RevokeAndAck, htlcsum from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture
from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
funding_output_script, get_per_commitment_secret_from_seed, funding_output_script, get_per_commitment_secret_from_seed,
@ -720,7 +720,8 @@ class Peer(Logger):
latest_remote_ctn = chan.hm.ctn_latest(REMOTE) latest_remote_ctn = chan.hm.ctn_latest(REMOTE)
next_remote_ctn = latest_remote_ctn + 1 next_remote_ctn = latest_remote_ctn + 1
# send message # 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: if oldest_unrevoked_remote_ctn == 0:
last_rev_secret = 0 last_rev_secret = 0
else: else:
@ -746,6 +747,8 @@ class Peer(Logger):
chan.set_state('OPENING') chan.set_state('OPENING')
their_next_local_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big') 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_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_we_are_ahead = False
should_close_they_are_ahead = False should_close_they_are_ahead = False
@ -786,9 +789,34 @@ class Peer(Logger):
should_close_we_are_ahead = True should_close_we_are_ahead = True
else: else:
should_close_they_are_ahead = True should_close_they_are_ahead = True
# option_data_loss_protect
# TODO 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: 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.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) self.try_to_get_remote_to_force_close_with_their_latest(chan_id)

5
electrum/lnsweep.py

@ -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) create_txns_for_htlc(htlc, is_received_htlc=True)
return txs 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, # note: the remote sometimes has two valid non-revoked commitment transactions,
# either of which could be broadcast (their_conf.ctn, their_conf.ctn+1) # 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) our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
@ -265,6 +265,9 @@ def analyze_ctx(chan, ctx):
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
is_revocation = True is_revocation = True
#_logger.info(f'tx for revoked: {list(txs.keys())}') #_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: else:
return return
return ctn, their_pcp, is_revocation, per_commitment_secret return ctn, their_pcp, is_revocation, per_commitment_secret

Loading…
Cancel
Save