1
0

fix sweeping chan after local force-close using cb

scenario:
- user opens a lightning channel and exports an "imported channel backup"
- user closes channel via local-force-close
  - local ctx is published, to_local output has user's funds and they are CSV-locked for days
- user restores wallet file from seed and imports channel backup
- new wallet file should be able to sweep coins from to_local output (after CSV expires)

This was not working previously, as the local_payment_basepoint was not included in the
imported channel backups, and the code was interpreting the lack of this as the channel not
having option_static_remotekey enabled. This resulted in lnutil.extract_ctn_from_tx
using an incorrect funder_payment_basepoint, and lnsweep not recognising the ctx due to
the garbage ctn value.

The imported channel backup serialisation format is slightly changed to include the
previously missing field, and its version number is bumped (0->1). We allow importing
both version 0 and version 1 backups, however v0 backups cannot handle the above
described scenario (they can only be used to request a remote-force-close).

Note that we were/are setting the missing local_payment_basepoint to the pubkey of
one of the wallet change addresses, which is bruteforceable if necessary, but I
think it is not worth the complexity to add this bruteforce logic. Also note
that the bruteforcing could only be done after the local-force-close was broadcast.

Ideally people with existing channels and already exported v0 backups should re-export
v1 backups... Not sure how to handle this.

closes https://github.com/spesmilo/electrum/issues/8516
This commit is contained in:
SomberNight
2023-07-14 14:21:50 +00:00
parent b04ade5d7d
commit 1a46460d11
5 changed files with 103 additions and 30 deletions

View File

@@ -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