diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 2bb70e08e..bf162fa9c 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -184,6 +184,7 @@ class AbstractChannel(Logger, ABC): funding_outpoint: Outpoint node_id: bytes # note that it might not be the full 33 bytes; for OCB it is only the prefix _state: ChannelState + sweep_address: str def set_short_channel_id(self, short_id: ShortChannelID) -> None: self.short_channel_id = short_id @@ -289,10 +290,10 @@ class AbstractChannel(Logger, ABC): if self._sweep_info.get(txid) is None: our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx) their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx) - if our_sweep_info is not None: + if our_sweep_info: self._sweep_info[txid] = our_sweep_info self.logger.info(f'we (local) force closed') - elif their_sweep_info is not None: + elif their_sweep_info: self._sweep_info[txid] = their_sweep_info self.logger.info(f'they (remote) force closed.') else: @@ -300,6 +301,12 @@ class AbstractChannel(Logger, ABC): self.logger.info(f'not sure who closed.') return self._sweep_info[txid] + def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: + return None + + def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: + return + def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo, closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None: # note: state transitions are irreversible, but @@ -479,15 +486,21 @@ class ChannelBackup(AbstractChannel): Logger.__init__(self) self.config = {} if self.is_imported: + assert isinstance(cb, ImportedChannelBackupStorage) self.init_config(cb) self.unconfirmed_closing_txid = None # not a state, only for GUI - def init_config(self, cb): + def init_config(self, cb: ImportedChannelBackupStorage): + local_payment_pubkey = cb.local_payment_pubkey + if local_payment_pubkey is None: + self.logger.warning( + f"local_payment_pubkey missing from (old-type) channel backup. " + f"You should export and re-import a newer backup.") self.config[LOCAL] = LocalConfig.from_seed( channel_seed=cb.channel_seed, to_self_delay=cb.local_delay, + static_remotekey=local_payment_pubkey, # dummy values - static_remotekey=None, dust_limit_sat=None, max_htlc_value_in_flight_msat=None, max_accepted_htlcs=None, @@ -580,8 +593,6 @@ class ChannelBackup(AbstractChannel): @property def sweep_address(self) -> str: - # Since channel backups do not save the static_remotekey, payment_basepoint in - # their local config is not static) return self.lnworker.wallet.get_new_sweep_address_for_channel() def get_local_pubkey(self) -> bytes: diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 10151c33c..91b672899 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -206,6 +206,7 @@ def create_sweeptxs_for_our_ctx( to_local_witness_script = make_commitment_output_to_local_witness_script( their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script) + to_remote_address = None # test if this is our_ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) if not chan.is_backup(): @@ -359,6 +360,7 @@ def create_sweeptxs_for_their_ctx( witness_script = make_commitment_output_to_local_witness_script( our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', witness_script) + to_remote_address = None # test if this is their ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) if not chan.is_backup(): diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 2c3b6b1bb..0447034bf 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -31,6 +31,7 @@ from .i18n import _ from .lnaddr import lndecode from .bip32 import BIP32Node, BIP32_PRIME from .transaction import BCDataStream, OPPushDataGeneric +from .logging import get_logger if TYPE_CHECKING: @@ -39,6 +40,9 @@ if TYPE_CHECKING: from .lnonion import OnionRoutingFailure +_logger = get_logger(__name__) + + # defined in BOLT-03: HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 @@ -192,7 +196,7 @@ class LocalConfig(ChannelConfig): per_commitment_secret_seed = attr.ib(type=bytes, converter=hex_to_bytes) @classmethod - def from_seed(self, **kwargs): + def from_seed(cls, **kwargs): channel_seed = kwargs['channel_seed'] static_remotekey = kwargs.pop('static_remotekey') node = BIP32Node.from_rootseed(channel_seed, xtype='standard') @@ -202,7 +206,11 @@ class LocalConfig(ChannelConfig): kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE) kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE) kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE) - kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) if static_remotekey else keypair_generator(LnKeyFamily.PAYMENT_BASE) + if static_remotekey: + kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) + else: + # we expect all our channels to use option_static_remotekey, so ending up here likely indicates an issue... + kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE) return LocalConfig(**kwargs) def validate_params(self, *, funding_sat: int) -> None: @@ -236,7 +244,9 @@ class ChannelConstraints(StoredObject): funding_txn_minimum_depth = attr.ib(type=int) -CHANNEL_BACKUP_VERSION = 0 +CHANNEL_BACKUP_VERSION_LATEST = 1 +KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1,) +assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS @attr.s class ChannelBackupStorage(StoredObject): @@ -255,13 +265,13 @@ class ChannelBackupStorage(StoredObject): @stored_in('onchain_channel_backups') @attr.s class OnchainChannelBackupStorage(ChannelBackupStorage): - node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) + node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey @stored_in('imported_channel_backups') @attr.s class ImportedChannelBackupStorage(ChannelBackupStorage): - node_id = attr.ib(type=bytes, converter=hex_to_bytes) - privkey = attr.ib(type=bytes, converter=hex_to_bytes) + node_id = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey + privkey = attr.ib(type=bytes, converter=hex_to_bytes) # local node privkey host = attr.ib(type=str) port = attr.ib(type=int, converter=int) channel_seed = attr.ib(type=bytes, converter=hex_to_bytes) @@ -269,10 +279,11 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): remote_delay = attr.ib(type=int, converter=int) remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) + local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] def to_bytes(self) -> bytes: vds = BCDataStream() - vds.write_uint16(CHANNEL_BACKUP_VERSION) + vds.write_uint16(CHANNEL_BACKUP_VERSION_LATEST) vds.write_boolean(self.is_initiator) vds.write_bytes(self.privkey, 32) vds.write_bytes(self.channel_seed, 32) @@ -286,6 +297,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): vds.write_uint16(self.remote_delay) vds.write_string(self.host) vds.write_uint16(self.port) + vds.write_bytes(self.local_payment_pubkey, 33) return bytes(vds.input) @staticmethod @@ -293,22 +305,40 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): vds = BCDataStream() vds.write(s) version = vds.read_uint16() - if version != CHANNEL_BACKUP_VERSION: + if version not in KNOWN_CHANNEL_BACKUP_VERSIONS: raise Exception(f"unknown version for channel backup: {version}") + is_initiator = vds.read_boolean() + privkey = vds.read_bytes(32) + channel_seed = vds.read_bytes(32) + node_id = vds.read_bytes(33) + funding_txid = vds.read_bytes(32).hex() + funding_index = vds.read_uint16() + funding_address = vds.read_string() + remote_payment_pubkey = vds.read_bytes(33) + remote_revocation_pubkey = vds.read_bytes(33) + local_delay = vds.read_uint16() + remote_delay = vds.read_uint16() + host = vds.read_string() + port = vds.read_uint16() + if version >= 1: + local_payment_pubkey = vds.read_bytes(33) + else: + local_payment_pubkey = None return ImportedChannelBackupStorage( - is_initiator=vds.read_boolean(), - privkey=vds.read_bytes(32), - channel_seed=vds.read_bytes(32), - node_id=vds.read_bytes(33), - funding_txid=vds.read_bytes(32).hex(), - funding_index=vds.read_uint16(), - funding_address=vds.read_string(), - remote_payment_pubkey=vds.read_bytes(33), - remote_revocation_pubkey=vds.read_bytes(33), - local_delay=vds.read_uint16(), - remote_delay=vds.read_uint16(), - host=vds.read_string(), - port=vds.read_uint16(), + is_initiator=is_initiator, + privkey=privkey, + channel_seed=channel_seed, + node_id=node_id, + funding_txid=funding_txid, + funding_index=funding_index, + funding_address=funding_address, + remote_payment_pubkey=remote_payment_pubkey, + remote_revocation_pubkey=remote_revocation_pubkey, + local_delay=local_delay, + remote_delay=remote_delay, + host=host, + port=port, + local_payment_pubkey=local_payment_pubkey, ) @staticmethod diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 10f71ae8d..3914decfe 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2537,7 +2537,7 @@ class LNWallet(LNWorker): feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE return max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4) - def create_channel_backup(self, channel_id): + def create_channel_backup(self, channel_id: bytes): chan = self._channels[channel_id] # do not backup old-style channels assert chan.is_static_remotekey_enabled() @@ -2556,7 +2556,9 @@ class LNWallet(LNWorker): local_delay = chan.config[LOCAL].to_self_delay, remote_delay = chan.config[REMOTE].to_self_delay, remote_revocation_pubkey = chan.config[REMOTE].revocation_basepoint.pubkey, - remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey) + remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey, + local_payment_pubkey=chan.config[LOCAL].payment_basepoint.pubkey, + ) def export_channel_backup(self, channel_id): xpub = self.wallet.get_fingerprint() diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 30926bcb3..592b9f7a2 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -921,7 +921,7 @@ class TestLNUtil(ElectrumTestCase): self.assertEqual(ChannelType(0b10000000001000000000000), channel_type) @as_testnet - async def test_decode_imported_channel_backup(self): + async def test_decode_imported_channel_backup_v0(self): encrypted_cb = "channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==" config = SimpleConfig({'electrum_path': self.electrum_path}) d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) @@ -942,6 +942,34 @@ class TestLNUtil(ElectrumTestCase): remote_delay=720, remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + local_payment_pubkey=None, + ), + decoded_cb, + ) + + @as_testnet + async def test_decode_imported_channel_backup_v1(self): + encrypted_cb = "channel_backup:AVYIedu0qSLfY2M2bBxF6dA4RAxcmobp+3h9mxALWWsv5X7hhNg0XYOKNd11FE6BJOZgZnIZ4CCAlHtLNj0/9S5GbNhbNZiQXxeHMwC1lHvtjawkwSejIJyOI52DkDFHBAGZRd4fJjaPJRHnUizWfySVR4zjd08lTinpoIeL7C7tXBW1N6YqceqV7RpeoywlBXJtFfCCuw0hnUKgq3SMlBKapkNAIgGrg15aIHNcYeENxCxr5FD1s7DIwFSECqsBVnu/Ogx2oii8BfuxqJq8vuGq4Ib/BVaSVtdb2E1wklAor/CG0p9Fg9mFWND98JD+64nz9n/knPFFyHxTXErn+ct3ZcStsLYynWKUIocgu38PtzCJ7r5ivqOw4O49fbbzdjcgMUGklPYxjuinETneCo+dCPa1uepOGTqeOYmnjVYtYZYXOlWV1F5OtNoM7MwwJjAbz84=" + config = SimpleConfig({'electrum_path': self.electrum_path}) + d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) + wallet1 = d['wallet'] # type: Standard_Wallet + decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint()) + self.assertEqual( + ImportedChannelBackupStorage( + funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe', + funding_index=1, + funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp', + is_initiator=True, + node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'), + privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'), + host='195.201.207.61', + port=9739, + channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'), + local_delay=1008, + remote_delay=720, + remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), + remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'), ), decoded_cb, )