Skip to content

Commit

Permalink
resumable channels:
Browse files Browse the repository at this point in the history
 - client and server are distinct roles, with different flags
 - the client sends a signature of his channel state with each
   commitment_signed or revoke_and_ack message.
 - the server reconstructs the state and verifies those signatures
 - actually, the state is made of two parts: one is signed with cs, one with revack
 - in unit tests, both peers are client and server
  • Loading branch information
ecdsa committed Apr 25, 2024
1 parent 2a4c5d9 commit d0d6f9d
Show file tree
Hide file tree
Showing 12 changed files with 664 additions and 72 deletions.
374 changes: 371 additions & 3 deletions electrum/lnchannel.py

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions electrum/lnhtlc.py
Expand Up @@ -168,19 +168,20 @@ def send_rev(self) -> None:
self.log[LOCAL]['ctn'] += 1
self._set_revack_pending(LOCAL, False)
self.log[LOCAL]['was_revoke_last'] = True
# htlcs
# htlcs proposed by remote, update locked_in in remote_ctx
for htlc_id in self._maybe_active_htlc_ids[REMOTE]:
ctns = self.log[REMOTE]['locked_in'][htlc_id]
if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):
ctns[REMOTE] = self.ctn_latest(REMOTE) + 1
# htlcs proposed by local, update settles/fails in remote_ctx
for log_action in ('settles', 'fails'):
for htlc_id in self._maybe_active_htlc_ids[LOCAL]:
ctns = self.log[LOCAL][log_action].get(htlc_id, None)
if ctns is None: continue
if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):
ctns[REMOTE] = self.ctn_latest(REMOTE) + 1
self._update_maybe_active_htlc_ids()
# fee updates
# fee updates proposed by remote: update remote_ctn
for k, fee_update in list(self.log[REMOTE]['fee_updates'].items()):
if fee_update.ctn_remote is None and fee_update.ctn_local <= self.ctn_latest(LOCAL):
fee_update.ctn_remote = self.ctn_latest(REMOTE) + 1
Expand All @@ -189,19 +190,20 @@ def send_rev(self) -> None:
def recv_rev(self) -> None:
self.log[REMOTE]['ctn'] += 1
self._set_revack_pending(REMOTE, False)
# htlcs
# htlcs proposed by local, update locked_in in local_ctx
for htlc_id in self._maybe_active_htlc_ids[LOCAL]:
ctns = self.log[LOCAL]['locked_in'][htlc_id]
if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):
ctns[LOCAL] = self.ctn_latest(LOCAL) + 1
# htlcs proposed by remote, update settles/fails in local_ctx
for log_action in ('settles', 'fails'):
for htlc_id in self._maybe_active_htlc_ids[REMOTE]:
ctns = self.log[REMOTE][log_action].get(htlc_id, None)
if ctns is None: continue
if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):
ctns[LOCAL] = self.ctn_latest(LOCAL) + 1
self._update_maybe_active_htlc_ids()
# fee updates
# fee updates proposed by local
for k, fee_update in list(self.log[LOCAL]['fee_updates'].items()):
if fee_update.ctn_local is None and fee_update.ctn_remote <= self.ctn_latest(REMOTE):
fee_update.ctn_local = self.ctn_latest(LOCAL) + 1
Expand Down
193 changes: 157 additions & 36 deletions electrum/lnpeer.py

Large diffs are not rendered by default.

50 changes: 46 additions & 4 deletions electrum/lnutil.py
Expand Up @@ -102,6 +102,10 @@ class ChannelConfig(StoredObject):
upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
announcement_node_sig = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
announcement_bitcoin_sig = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
current_commitment_signature = attr.ib(type=bytes, converter=hex_to_bytes)
current_htlc_signatures = attr.ib(type=bytes, converter=hex_to_bytes)
next_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes)
current_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes)

def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None:
conf_name = type(self).__name__
Expand Down Expand Up @@ -205,8 +209,6 @@ def cross_validate_params(
class LocalConfig(ChannelConfig):
channel_seed = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex) # type: Optional[bytes]
funding_locked_received = attr.ib(type=bool)
current_commitment_signature = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
current_htlc_signatures = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
per_commitment_secret_seed = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)

@classmethod
Expand Down Expand Up @@ -240,8 +242,7 @@ def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_feat
@stored_as('remote_config')
@attr.s
class RemoteConfig(ChannelConfig):
next_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
current_per_commitment_point = attr.ib(default=None, type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)
encrypted_seed = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes]

@stored_in('fee_updates')
@attr.s
Expand Down Expand Up @@ -485,6 +486,19 @@ def __init__(self, storage):
self.storage = storage
self.buckets = storage['buckets']

@classmethod
def from_seed_and_index(cls, seed, index):
# this is vastly inefficient, but ok for now...
storage = {
'index': RevocationStore.START_INDEX,
'buckets': {},
}
s = RevocationStore(storage)
for i in range(index):
secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - i)
s.add_next_entry(secret)
return s

def add_next_entry(self, hsh):
index = self.storage['index']
new_element = ShachainElement(index=index, secret=hsh)
Expand All @@ -510,6 +524,9 @@ def retrieve_secret(self, index: int) -> bytes:
return element.secret
raise UnableToDeriveSecret()

def serialize(self):
pass

def __eq__(self, o):
return type(o) is RevocationStore and self.serialize() == o.serialize()

Expand Down Expand Up @@ -559,6 +576,18 @@ def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) ->
bajts = bytes(per_commitment_secret)
return bajts

def get_pcp_from_seed(seed: bytes, i: int) -> bytes:
per_commitment_secret = get_per_commitment_secret_from_seed(seed, i)
return secret_to_pubkey(int.from_bytes(per_commitment_secret, 'big'))

def get_revocations_from_seed(seed:bytes, ctn:int):
revocations = b''
for i in range(49):
ctn_i = pow(2, i)
secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - ctn_i)
revocations += ctn
return revocations

def secret_to_pubkey(secret: int) -> bytes:
assert type(secret) is int
return ecc.ECPrivkey.from_secret_scalar(secret).get_public_key_bytes(compressed=True)
Expand Down Expand Up @@ -1223,6 +1252,18 @@ class LnFeatures(IntFlag):
_ln_feature_contexts[OPTION_ZEROCONF_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
_ln_feature_contexts[OPTION_ZEROCONF_OPT] = (LNFC.INIT | LNFC.NODE_ANN)

OPTION_ELECTRUM_PEERBACKUP_CLIENT_REQ = 1 << 152
OPTION_ELECTRUM_PEERBACKUP_CLIENT_OPT = 1 << 153

_ln_feature_contexts[OPTION_ELECTRUM_PEERBACKUP_CLIENT_REQ] = (LNFC.INIT)
_ln_feature_contexts[OPTION_ELECTRUM_PEERBACKUP_CLIENT_OPT] = (LNFC.INIT)

OPTION_ELECTRUM_PEERBACKUP_SERVER_REQ = 1 << 154
OPTION_ELECTRUM_PEERBACKUP_SERVER_OPT = 1 << 155

_ln_feature_contexts[OPTION_ELECTRUM_PEERBACKUP_SERVER_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
_ln_feature_contexts[OPTION_ELECTRUM_PEERBACKUP_SERVER_OPT] = (LNFC.INIT | LNFC.NODE_ANN)

def validate_transitive_dependencies(self) -> bool:
# for all even bit set, set corresponding odd bit:
features = self # copy
Expand Down Expand Up @@ -1384,6 +1425,7 @@ def name_minimal(self):
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ
| LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ
| LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_REQ
| LnFeatures.OPTION_ELECTRUM_PEERBACKUP_CLIENT_OPT | LnFeatures.OPTION_ELECTRUM_PEERBACKUP_SERVER_OPT
)


Expand Down
27 changes: 27 additions & 0 deletions electrum/lnwire/peer_wire.csv
Expand Up @@ -63,6 +63,8 @@ tlvtype,open_channel_tlvs,channel_type,1
tlvdata,open_channel_tlvs,channel_type,type,byte,...
tlvtype,open_channel_tlvs,channel_opening_fee,10000
tlvdata,open_channel_tlvs,channel_opening_fee,channel_opening_fee,u64,
tlvtype,open_channel_tlvs,encrypted_seed,10001
tlvdata,open_channel_tlvs,encrypted_seed,seed,byte,...
msgtype,accept_channel,33
msgdata,accept_channel,temporary_channel_id,byte,32
msgdata,accept_channel,dust_limit_satoshis,u64,
Expand All @@ -83,6 +85,8 @@ tlvtype,accept_channel_tlvs,upfront_shutdown_script,0
tlvdata,accept_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,...
tlvtype,accept_channel_tlvs,channel_type,1
tlvdata,accept_channel_tlvs,channel_type,type,byte,...
tlvtype,accept_channel_tlvs,encrypted_seed,2
tlvdata,accept_channel_tlvs,encrypted_seed,seed,byte,...
msgtype,funding_created,34
msgdata,funding_created,temporary_channel_id,byte,32
msgdata,funding_created,funding_txid,sha256,
Expand Down Expand Up @@ -135,10 +139,26 @@ msgdata,commitment_signed,channel_id,channel_id,
msgdata,commitment_signed,signature,signature,
msgdata,commitment_signed,num_htlcs,u16,
msgdata,commitment_signed,htlc_signature,signature,num_htlcs
msgdata,commitment_signed,tlvs,commitment_signed_tlvs,
tlvtype,commitment_signed_tlvs,peerbackup,1
tlvdata,commitment_signed_tlvs,peerbackup,signature,signature,
tlvtype,commitment_signed_tlvs,time_commitment,2
tlvdata,commitment_signed_tlvs,time_commitment,timestamp,u64,
tlvdata,commitment_signed_tlvs,time_commitment,local_ctn,u64,
tlvdata,commitment_signed_tlvs,time_commitment,remote_ctn,u64,
tlvdata,commitment_signed_tlvs,time_commitment,signature,signature,
msgtype,revoke_and_ack,133
msgdata,revoke_and_ack,channel_id,channel_id,
msgdata,revoke_and_ack,per_commitment_secret,byte,32
msgdata,revoke_and_ack,next_per_commitment_point,point,
msgdata,revoke_and_ack,tlvs,revoke_and_ack_tlvs,
tlvtype,revoke_and_ack_tlvs,peerbackup,1
tlvdata,revoke_and_ack_tlvs,peerbackup,signature,signature,
tlvtype,revoke_and_ack_tlvs,time_commitment,2
tlvdata,revoke_and_ack_tlvs,time_commitment,timestamp,u64,
tlvdata,revoke_and_ack_tlvs,time_commitment,local_ctn,u64,
tlvdata,revoke_and_ack_tlvs,time_commitment,remote_ctn,u64,
tlvdata,revoke_and_ack_tlvs,time_commitment,signature,signature,
msgtype,update_fee,134
msgdata,update_fee,channel_id,channel_id,
msgdata,update_fee,feerate_per_kw,u32,
Expand All @@ -148,6 +168,13 @@ msgdata,channel_reestablish,next_commitment_number,u64,
msgdata,channel_reestablish,next_revocation_number,u64,
msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32
msgdata,channel_reestablish,my_current_per_commitment_point,point,
msgdata,channel_reestablish,tlvs,channel_reestablish_tlvs,
tlvtype,channel_reestablish_tlvs,your_local_peerbackup,100
tlvdata,channel_reestablish_tlvs,your_local_peerbackup,signature,signature,
tlvdata,channel_reestablish_tlvs,your_local_peerbackup,state,byte,...
tlvtype,channel_reestablish_tlvs,your_remote_peerbackup,101
tlvdata,channel_reestablish_tlvs,your_remote_peerbackup,signature,signature,
tlvdata,channel_reestablish_tlvs,your_remote_peerbackup,state,byte,...
msgtype,announcement_signatures,259
msgdata,announcement_signatures,channel_id,channel_id,
msgdata,announcement_signatures,short_channel_id,short_channel_id,
Expand Down
7 changes: 5 additions & 2 deletions electrum/lnworker.py
Expand Up @@ -224,6 +224,7 @@ class ErrorAddingPeer(Exception): pass
| LnFeatures.OPTION_CHANNEL_TYPE_OPT
| LnFeatures.OPTION_SCID_ALIAS_OPT
| LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT
| LnFeatures.OPTION_ELECTRUM_PEERBACKUP_CLIENT_OPT
)

LNGOSSIP_FEATURES = (
Expand Down Expand Up @@ -814,6 +815,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv):
features = LNWALLET_FEATURES
if self.config.ACCEPT_ZEROCONF_CHANNELS:
features |= LnFeatures.OPTION_ZEROCONF_OPT
if self.config.LIGHTNING_PEERBACKUP_SERVER:
features |= LnFeatures.OPTION_ELECTRUM_PEERBACKUP_SERVER_OPT
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
self.lnwatcher = None
self.lnrater: LNRater = None
Expand Down Expand Up @@ -858,7 +861,6 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv):
self.payment_bundles = [] # lists of hashes. todo:persist
self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self)


def has_deterministic_node_id(self) -> bool:
return bool(self.db.get('lightning_xprv'))

Expand Down Expand Up @@ -1040,7 +1042,8 @@ def get_lightning_history(self):
label = self.wallet.get_label_for_rhash(key)
if not label and direction == PaymentDirection.FORWARDING:
label = _('Forwarding')
preimage = self.get_preimage(payment_hash).hex()
preimage = self.get_preimage(payment_hash)
preimage = preimage.hex() if preimage else None
item = {
'type': 'payment',
'label': label,
Expand Down
3 changes: 3 additions & 0 deletions electrum/simple_config.py
Expand Up @@ -1195,6 +1195,9 @@ def _default_swapserver_url(self) -> str:
ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)

# serve channel backups
LIGHTNING_PEERBACKUP_SERVER = ConfigVar('lightning_peerbackup_server', default=False, type_=bool)

# connect to remote WT
WATCHTOWER_CLIENT_ENABLED = ConfigVar(
'use_watchtower', default=False, type_=bool,
Expand Down
16 changes: 15 additions & 1 deletion electrum/wallet_db.py
Expand Up @@ -72,7 +72,7 @@ def __init__(self, wallet_db: 'WalletDB'):
# seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 59 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 60 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format


Expand Down Expand Up @@ -234,6 +234,7 @@ def upgrade(self):
self._convert_version_57()
self._convert_version_58()
self._convert_version_59()
self._convert_version_60()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure

def _convert_wallet_type(self):
Expand Down Expand Up @@ -1146,6 +1147,19 @@ def _convert_version_59(self):
self.data['channels'] = channels
self.data['seed_version'] = 59

def _convert_version_60(self):
if not self._is_upgrade_method_needed(59, 59):
return
channels = self.data.get('channels', {})
for _key, chan in channels.items():
chan['local_config']['next_per_commitment_point'] = None
chan['local_config']['current_per_commitment_point'] = None
chan['remote_config']['current_commitment_signature'] = None
chan['remote_config']['current_htlc_signatures'] = None
chan['remote_config']['encrypted_seed'] = None
self.data['channels'] = channels
self.data['seed_version'] = 60

def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
Expand Down
1 change: 1 addition & 0 deletions tests/regtest.py
Expand Up @@ -123,6 +123,7 @@ class TestLightningJIT(TestLightning):
'lightning_listen': 'localhost:9735',
'lightning_forward_payments': 'true',
'accept_zeroconf_channels': 'true',
'lightning_peerbackup_server': 'true',
},
'carol':{
}
Expand Down
5 changes: 5 additions & 0 deletions tests/test_lnchannel.py
Expand Up @@ -74,6 +74,9 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
upfront_shutdown_script=b'',
announcement_node_sig=b'',
announcement_bitcoin_sig=b'',
current_commitment_signature=None,
current_htlc_signatures=None,
encrypted_seed=None,
),
"local_config":lnpeer.LocalConfig(
channel_seed = None,
Expand All @@ -96,6 +99,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
upfront_shutdown_script=b'',
announcement_node_sig=b'',
announcement_bitcoin_sig=b'',
next_per_commitment_point=None,
current_per_commitment_point=None,
),
"constraints":lnpeer.ChannelConstraints(
flags=0,
Expand Down

0 comments on commit d0d6f9d

Please sign in to comment.