implement data_loss_protect
so that we can spend their_ctx_to_remote even when we lost our state but have an old backup
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
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
|
||||||
|
|
||||||
# TODO option_data_loss_protect
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user