Browse Source

Merge pull request #8536 from SomberNight/202307_ln_imported_cb

fix sweeping chan after local force-close using cb
master
ThomasV 2 years ago committed by GitHub
parent
commit
a6a2679d8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      electrum/lnchannel.py
  2. 2
      electrum/lnsweep.py
  3. 72
      electrum/lnutil.py
  4. 6
      electrum/lnworker.py
  5. 3
      electrum/tests/regtest.py
  6. 25
      electrum/tests/regtest/regtest.sh
  7. 30
      electrum/tests/test_lnutil.py

23
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:

2
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():

72
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

6
electrum/lnworker.py

@ -2529,7 +2529,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()
@ -2548,7 +2548,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()

3
electrum/tests/regtest.py

@ -56,6 +56,9 @@ class TestLightningAB(TestLightning):
def test_backup(self):
self.run_shell(['backup'])
def test_backup_local_forceclose(self):
self.run_shell(['backup_local_forceclose'])
def test_breach(self):
self.run_shell(['breach'])

25
electrum/tests/regtest/regtest.sh

@ -171,6 +171,31 @@ if [[ $1 == "backup" ]]; then
fi
if [[ $1 == "backup_local_forceclose" ]]; then
# Alice does a local-force-close, and then restores from seed before sweeping CSV-locked coins
wait_for_balance alice 1
echo "alice opens channel"
bob_node=$($bob nodeid)
$alice setconfig use_recoverable_channels False
channel=$($alice open_channel $bob_node 0.15)
new_blocks 3
wait_until_channel_open alice
backup=$($alice export_channel_backup $channel)
echo "local force close $channel"
$alice close_channel $channel --force
sleep 0.5
seed=$($alice getseed)
$alice stop
mv /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/default_wallet.old
new_blocks 150
$alice -o restore "$seed"
$alice daemon -d
$alice load_wallet
$alice import_channel_backup $backup
wait_for_balance alice 0.998
fi
if [[ $1 == "collaborative_close" ]]; then
wait_for_balance alice 1
echo "alice opens channel"

30
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,
)

Loading…
Cancel
Save