Merge pull request #8536 from SomberNight/202307_ln_imported_cb
fix sweeping chan after local force-close using cb
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user