From 816e617aafe9ff741c7a3219b6071d19de03ccf1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 8 Aug 2023 05:09:58 +0200 Subject: [PATCH 1/3] option_zeroconf - accept zeroconf channels only from a single node - fw_info uses get_scid_or_local_alias --- electrum/commands.py | 4 +++- electrum/lnchannel.py | 9 ++++++++- electrum/lnpeer.py | 23 ++++++++++++++++++++--- electrum/lnutil.py | 7 +++++++ electrum/lnworker.py | 13 ++++++++++--- electrum/simple_config.py | 4 ++++ 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index aab0793f2..9b5647ed3 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1137,7 +1137,7 @@ class Commands: } for p in lnworker.peers.values()] @command('wpnl') - async def open_channel(self, connection_string, amount, push_amount=0, public=False, password=None, wallet: Abstract_Wallet = None): + async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None): funding_sat = satoshis(amount) push_sat = satoshis(push_amount) peer = await wallet.lnworker.add_peer(connection_string) @@ -1145,6 +1145,7 @@ class Commands: peer, funding_sat, push_sat=push_sat, public=public, + zeroconf=zeroconf, password=password) return chan.funding_outpoint.to_str() @@ -1449,6 +1450,7 @@ command_options = { 'force': (None, "Create new address beyond gap limit, if no more addresses are available."), 'pending': (None, "Show only pending requests."), 'push_amount': (None, 'Push initial amount (in BTC)'), + 'zeroconf': (None, 'request zeroconf channel'), 'expired': (None, "Show only expired requests."), 'paid': (None, "Show only paid requests."), 'show_addresses': (None, "Show input and output addresses"), diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index b9ce1915f..9d1bec3f3 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -695,6 +695,9 @@ class Channel(AbstractChannel): alias = self.storage.get('alias') return bytes.fromhex(alias) if alias else None + def get_scid_or_local_alias(self): + return self.short_channel_id or self.get_local_scid_alias() + def has_onchain_backup(self): return self.storage.get('has_onchain_backup', False) @@ -831,6 +834,10 @@ class Channel(AbstractChannel): channel_type = ChannelType(self.storage.get('channel_type')) return bool(channel_type & ChannelType.OPTION_STATIC_REMOTEKEY) + def is_zeroconf(self) -> bool: + channel_type = ChannelType(self.storage.get('channel_type')) + return bool(channel_type & ChannelType.OPTION_ZEROCONF) + @property def sweep_address(self) -> str: # TODO: in case of unilateral close with pending HTLCs, this address will be reused @@ -1696,7 +1703,7 @@ class Channel(AbstractChannel): if conf < self.funding_txn_minimum_depth(): #self.logger.info(f"funding tx is still not at sufficient depth. actual depth: {conf}") return False - assert conf > 0 + assert conf > 0 or self.is_zeroconf() # check funding_tx amount and script funding_tx = self.lnworker.lnwatcher.adb.get_transaction(funding_txid) if not funding_tx: diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index ee473e087..f80dc2a62 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -627,6 +627,9 @@ class Peer(Logger): def is_channel_type(self): return self.features.supports(LnFeatures.OPTION_CHANNEL_TYPE_OPT) + def accepts_zeroconf(self): + return self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT) + def is_upfront_shutdown_script(self): return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT) @@ -710,6 +713,7 @@ class Peer(Logger): funding_sat: int, push_msat: int, public: bool, + zeroconf: bool = False, temp_channel_id: bytes ) -> Tuple[Channel, 'PartialTransaction']: """Implements the channel opening flow. @@ -736,6 +740,8 @@ class Peer(Logger): open_channel_tlvs = {} assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY) + if zeroconf: + our_channel_type |= ChannelType(ChannelType.OPTION_ZEROCONF) # We do not set the option_scid_alias bit in channel_type because LND rejects it. # Eclair accepts channel_type with that bit, but does not require it. @@ -791,7 +797,7 @@ class Peer(Logger): self.logger.debug(f"received accept_channel for temp_channel_id={temp_channel_id.hex()}. {payload=}") remote_per_commitment_point = payload['first_per_commitment_point'] funding_txn_minimum_depth = payload['minimum_depth'] - if funding_txn_minimum_depth <= 0: + if not zeroconf and funding_txn_minimum_depth <= 0: raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}") if funding_txn_minimum_depth > 30: raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}") @@ -903,6 +909,9 @@ class Peer(Logger): await self.send_warning(channel_id, message=str(e), close_connection=True) chan.open_with_first_pcp(remote_per_commitment_point, remote_sig) chan.set_state(ChannelState.OPENING) + if zeroconf: + chan.set_state(ChannelState.FUNDED) + self.send_channel_ready(chan) self.lnworker.add_new_channel(chan) return chan, funding_tx @@ -1009,7 +1018,12 @@ class Peer(Logger): ) per_commitment_point_first = secret_to_pubkey( int.from_bytes(per_commitment_secret_first, 'big')) - min_depth = 3 + + is_zeroconf = channel_type & channel_type.OPTION_ZEROCONF + if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()): + raise Exception(f"not accepting zeroconf from node {self.pubkey}") + min_depth = 0 if is_zeroconf else 3 + accept_channel_tlvs = { 'upfront_shutdown_script': { 'shutdown_scriptpubkey': local_config.upfront_shutdown_script @@ -1078,6 +1092,9 @@ class Peer(Logger): self.funding_signed_sent.add(chan.channel_id) chan.open_with_first_pcp(payload['first_per_commitment_point'], remote_sig) chan.set_state(ChannelState.OPENING) + if is_zeroconf: + chan.set_state(ChannelState.FUNDED) + self.send_channel_ready(chan) self.lnworker.add_new_channel(chan) async def request_force_close(self, channel_id: bytes): @@ -1421,7 +1438,7 @@ class Peer(Logger): chan.set_remote_update(pending_channel_update) self.logger.info(f"CHANNEL OPENING COMPLETED ({chan.get_id_for_log()})") forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS - if forwarding_enabled: + if forwarding_enabled and chan.short_channel_id: # send channel_update of outgoing edge to peer, # so that channel can be used to to receive payments self.logger.info(f"sending channel update for outgoing edge ({chan.get_id_for_log()})") diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 22d144717..eb34eefc7 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1206,6 +1206,13 @@ class LnFeatures(IntFlag): _ln_feature_contexts[OPTION_SCID_ALIAS_REQ] = (LNFC.INIT | LNFC.NODE_ANN) _ln_feature_contexts[OPTION_SCID_ALIAS_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + OPTION_ZEROCONF_REQ = 1 << 50 + OPTION_ZEROCONF_OPT = 1 << 51 + + _ln_feature_direct_dependencies[OPTION_ZEROCONF_OPT] = {OPTION_SCID_ALIAS_OPT} + _ln_feature_contexts[OPTION_ZEROCONF_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + _ln_feature_contexts[OPTION_ZEROCONF_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + def validate_transitive_dependencies(self) -> bool: # for all even bit set, set corresponding odd bit: features = self # copy diff --git a/electrum/lnworker.py b/electrum/lnworker.py index bbc9e378e..2e3139101 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -798,13 +798,16 @@ class LNWallet(LNWorker): def __init__(self, wallet: 'Abstract_Wallet', xprv): self.wallet = wallet + self.config = wallet.config self.db = wallet.db self.node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY) self.backup_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.BACKUP_CIPHER).privkey self.payment_secret_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_SECRET_KEY).privkey Logger.__init__(self) - LNWorker.__init__(self, self.node_keypair, LNWALLET_FEATURES) - self.config = wallet.config + features = LNWALLET_FEATURES + if self.config.ACCEPT_ZEROCONF_CHANNELS: + features |= LnFeatures.OPTION_ZEROCONF_OPT + LNWorker.__init__(self, self.node_keypair, features) self.lnwatcher = None self.lnrater: LNRater = None self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid @@ -1223,6 +1226,7 @@ class LNWallet(LNWorker): self, peer, funding_sat, *, push_sat: int = 0, public: bool = False, + zeroconf: bool = False, password=None): coins = self.wallet.get_spendable_coins(None) node_id = peer.pubkey @@ -1237,6 +1241,7 @@ class LNWallet(LNWorker): funding_sat=funding_sat, push_sat=push_sat, public=public, + zeroconf=zeroconf, password=password) return chan, funding_tx @@ -1248,6 +1253,7 @@ class LNWallet(LNWorker): funding_sat: int, push_sat: int, public: bool, + zeroconf=False, password: Optional[str]) -> Tuple[Channel, PartialTransaction]: coro = peer.channel_establishment_flow( @@ -1255,6 +1261,7 @@ class LNWallet(LNWorker): funding_sat=funding_sat, push_msat=push_sat * 1000, public=public, + zeroconf=zeroconf, temp_channel_id=os.urandom(32)) chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT) util.trigger_callback('channels_updated', self.wallet) @@ -2306,7 +2313,7 @@ class LNWallet(LNWorker): If we find this was a forwarded HTLC, the upstream peer is notified. Returns whether this was a forwarded HTLC. """ - fw_info = chan.short_channel_id.hex(), htlc_id + fw_info = chan.get_scid_or_local_alias().hex(), htlc_id upstream_peer_pubkey = self.downstream_htlc_to_upstream_peer_map.get(fw_info) if not upstream_peer_pubkey: return False diff --git a/electrum/simple_config.py b/electrum/simple_config.py index d4d19a296..454198add 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1169,6 +1169,10 @@ This will result in longer routes; it might increase your fees and decrease the SWAPSERVER_PORT = ConfigVar('swapserver_port', default=5455, type_=int) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) + # zeroconf + ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool) + ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str) + # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar( 'use_watchtower', default=False, type_=bool, From a338459d452d27e5176c718827a9c5ad304de8ce Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 8 Aug 2023 05:09:58 +0200 Subject: [PATCH 2/3] just-in-time channels: - a node scid alias is derived from the node ID - the channel opening fee is sent in a TLV field of open_channel - the server requires htlc settlement before broadcasting (server does not trust client) --- electrum/lnchannel.py | 3 +- electrum/lnpeer.py | 102 +++++++++++++++++++++++++++++- electrum/lnwire/peer_wire.csv | 2 + electrum/lnworker.py | 86 +++++++++++++++++++++++-- electrum/simple_config.py | 3 +- electrum/tests/regtest.py | 36 +++++++++++ electrum/tests/regtest/regtest.sh | 15 +++++ 7 files changed, 236 insertions(+), 11 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 9d1bec3f3..22f0e4f76 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -639,7 +639,8 @@ class Channel(AbstractChannel): def __repr__(self): return "Channel(%s)"%self.get_id_for_log() - def __init__(self, state: 'StoredDict', *, name=None, lnworker=None, initial_feerate=None): + def __init__(self, state: 'StoredDict', *, name=None, lnworker=None, initial_feerate=None, opening_fee=None): + self.opening_fee = opening_fee self.name = name self.channel_id = bfh(state["channel_id"]) self.short_channel_id = ShortChannelID.normalize(state["short_channel_id"]) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index f80dc2a62..70ab8d3b3 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -27,6 +27,7 @@ from . import transaction from .bitcoin import make_op_return, DummyAddress from .transaction import PartialTxOutput, match_script_against_template, Sighash from .logging import Logger +from .lnrouter import RouteEdge from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, process_onion_packet, OnionPacket, construct_onion_error, obfuscate_onion_error, OnionRoutingFailure, ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey, @@ -119,6 +120,7 @@ class Peer(Logger): self._received_revack_event = asyncio.Event() self.received_commitsig_event = asyncio.Event() self.downstream_htlc_resolved_event = asyncio.Event() + self.jit_failures = {} def send_message(self, message_name: str, **kwargs): assert util.get_running_loop() == util.get_asyncio_loop(), f"this must be run on the asyncio thread!" @@ -714,7 +716,8 @@ class Peer(Logger): push_msat: int, public: bool, zeroconf: bool = False, - temp_channel_id: bytes + temp_channel_id: bytes, + opening_fee: int = None, ) -> Tuple[Channel, 'PartialTransaction']: """Implements the channel opening flow. @@ -757,7 +760,11 @@ class Peer(Logger): open_channel_tlvs['upfront_shutdown_script'] = { 'shutdown_scriptpubkey': local_config.upfront_shutdown_script } - + if opening_fee: + # todo: maybe add payment hash + open_channel_tlvs['channel_opening_fee'] = { + 'channel_opening_fee': opening_fee + } # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed( local_config.per_commitment_secret_seed, @@ -963,6 +970,11 @@ class Peer(Logger): open_channel_tlvs = payload.get('open_channel_tlvs') channel_type = open_channel_tlvs.get('channel_type') if open_channel_tlvs else None + + channel_opening_fee = open_channel_tlvs.get('channel_opening_fee') if open_channel_tlvs else None + if channel_opening_fee: + # todo check that the fee is reasonable + pass # The receiving node MAY fail the channel if: # option_channel_type was negotiated but the message doesn't include a channel_type if self.is_channel_type() and channel_type is None: @@ -1073,7 +1085,8 @@ class Peer(Logger): chan = Channel( chan_dict, lnworker=self.lnworker, - initial_feerate=feerate + initial_feerate=feerate, + opening_fee = channel_opening_fee, ) chan.storage['init_timestamp'] = int(time.time()) if isinstance(self.transport, LNTransport): @@ -1714,7 +1727,36 @@ class Peer(Logger): next_cltv_abs = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') + next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) + + if self.lnworker.features.supports(LnFeatures.OPTION_ZEROCONF_OPT): + next_peer = self.lnworker.get_peer_by_scid_alias(next_chan_scid) + else: + next_peer = None + + if not next_chan and next_peer and next_peer.accepts_zeroconf(): + # check if an already existing channel can be used. + # todo: split the payment + for next_chan in next_peer.channels.values(): + if next_chan.can_pay(next_amount_msat_htlc): + break + else: + async def wrapped_callback(): + coro = self.lnworker.open_channel_just_in_time( + next_peer, + next_amount_msat_htlc, + next_cltv_abs, + htlc.payment_hash, + processed_onion.next_packet) + try: + await coro + except OnionRoutingFailure as e: + self.jit_failures[next_chan_scid.hex()] = e + + asyncio.ensure_future(wrapped_callback()) + return next_chan_scid, -1 + local_height = chain.height() if next_chan is None: log_fail_reason(f"cannot find next_chan {next_chan_scid}") @@ -1754,6 +1796,7 @@ class Peer(Logger): self.logger.info( f"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. " f"next_chan={next_chan.get_id_for_log()}.") + next_peer = self.lnworker.peers.get(next_chan.node_id) if next_peer is None: log_fail_reason(f"next_peer offline ({next_chan.node_id.hex()})") @@ -1837,6 +1880,43 @@ class Peer(Logger): if budget.cltv < 576: raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'') + # do we have a connection to the node? + next_peer = self.lnworker.peers.get(outgoing_node_id) + if next_peer and next_peer.accepts_zeroconf(): + self.logger.info(f'JIT: found next_peer') + for next_chan in next_peer.channels.values(): + if next_chan.can_pay(amt_to_forward): + # todo: detect if we can do mpp + self.logger.info(f'jit: next_chan can pay') + break + else: + scid_alias = self.lnworker._scid_alias_of_node(next_peer.pubkey) + route = [RouteEdge( + start_node=next_peer.pubkey, + end_node=outgoing_node_id, + short_channel_id=scid_alias, + fee_base_msat=0, + fee_proportional_millionths=0, + cltv_delta=144, + node_features=0 + )] + next_onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route( + route=route, + amount_msat=amt_to_forward, + total_msat=amt_to_forward, + payment_hash=payment_hash, + min_final_cltv_delta=cltv_budget_for_rest_of_route, + payment_secret=payment_secret, + trampoline_onion=next_trampoline_onion, + ) + await self.lnworker.open_channel_just_in_time( + next_peer, + amt_to_forward, + cltv_abs, + payment_hash, + next_onion) + return + try: await self.lnworker.pay_to_node( node_pubkey=outgoing_node_id, @@ -1926,6 +2006,13 @@ class Peer(Logger): log_fail_reason(f"'total_msat' missing from onion") raise exc_incorrect_or_unknown_pd + if chan.opening_fee: + channel_opening_fee = chan.opening_fee['channel_opening_fee'] + total_msat -= channel_opening_fee + amt_to_forward -= channel_opening_fee + else: + channel_opening_fee = 0 + if amt_to_forward > htlc.amount_msat: log_fail_reason(f"amt_to_forward != htlc.amount_msat") raise OnionRoutingFailure( @@ -2006,6 +2093,9 @@ class Peer(Logger): log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}') raise exc_incorrect_or_unknown_pd invoice_msat = info.amount_msat + if channel_opening_fee: + invoice_msat -= channel_opening_fee + if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}") raise exc_incorrect_or_unknown_pd @@ -2018,6 +2108,7 @@ class Peer(Logger): self.logger.info(f"missing preimage and no hold invoice callback {payment_hash.hex()}") raise exc_incorrect_or_unknown_pd + chan.opening_fee = None self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") return preimage, None @@ -2586,6 +2677,11 @@ class Peer(Logger): return None, None, error_bytes if error_reason: raise error_reason + # just-in-time channel + if htlc_id == -1: + error_reason = self.jit_failures.pop(next_chan_id_hex, None) + if error_reason: + raise error_reason if preimage: return preimage, None, None return None, None, None diff --git a/electrum/lnwire/peer_wire.csv b/electrum/lnwire/peer_wire.csv index b4018f0fc..063772c3d 100644 --- a/electrum/lnwire/peer_wire.csv +++ b/electrum/lnwire/peer_wire.csv @@ -61,6 +61,8 @@ tlvtype,open_channel_tlvs,upfront_shutdown_script,0 tlvdata,open_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,... 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, msgtype,accept_channel,33 msgdata,accept_channel,temporary_channel_id,byte,32 msgdata,accept_channel,dust_limit_satoshis,u64, diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 2e3139101..dfedebf90 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1221,12 +1221,72 @@ class LNWallet(LNWorker): self.logger.info('REBROADCASTING CLOSING TX') await self.network.try_broadcasting(force_close_tx, 'force-close') + def get_peer_by_scid_alias(self, scid_alias): + for nodeid, peer in self.peers.items(): + if scid_alias == self._scid_alias_of_node(nodeid): + return peer + + def _scid_alias_of_node(self, nodeid): + # scid alias for just-in-time channels + return sha256(b'Electrum' + nodeid)[0:8] + + def get_scid_alias(self): + return self._scid_alias_of_node(self.node_keypair.pubkey) + + @log_exceptions + async def open_channel_just_in_time(self, next_peer, next_amount_msat_htlc, next_cltv_abs, payment_hash, next_onion): + # if an exception is raised during negotiation, we raise an OnionRoutingFailure. + # this will cancel the incoming HTLC + try: + funding_sat = 2 * (next_amount_msat_htlc // 1000) # try to fully spend htlcs + password = self.wallet.get_unlocked_password() if self.wallet.has_password() else None + channel_opening_fee = next_amount_msat_htlc // 100 + if channel_opening_fee // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE: + self.logger.info(f'rejecting JIT channel: payment too low') + raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low') + self.logger.info(f'channel opening fee (sats): {channel_opening_fee//1000}') + next_chan, funding_tx = await self.open_channel_with_peer( + next_peer, funding_sat, + push_sat=0, + zeroconf=True, + public=False, + opening_fee=channel_opening_fee, + password=password, + ) + async def wait_for_channel(): + while not next_chan.is_open(): + await asyncio.sleep(1) + await util.wait_for2(wait_for_channel(), LN_P2P_NETWORK_TIMEOUT) + next_chan.save_remote_scid_alias(self._scid_alias_of_node(next_peer.pubkey)) + self.logger.info(f'JIT channel is open') + next_amount_msat_htlc -= channel_opening_fee + # fixme: some checks are missing + htlc = next_peer.send_htlc( + chan=next_chan, + payment_hash=payment_hash, + amount_msat=next_amount_msat_htlc, + cltv_abs=next_cltv_abs, + onion=next_onion) + async def wait_for_preimage(): + while self.get_preimage(payment_hash) is None: + await asyncio.sleep(1) + await util.wait_for2(wait_for_preimage(), LN_P2P_NETWORK_TIMEOUT) + except OnionRoutingFailure: + raise + except Exception: + raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') + # We have been paid and can broadcast + # if broadcasting raise an exception, we should try to rebroadcast + await self.network.broadcast_transaction(funding_tx) + return next_chan, funding_tx + @log_exceptions async def open_channel_with_peer( self, peer, funding_sat, *, push_sat: int = 0, public: bool = False, zeroconf: bool = False, + opening_fee: int = None, password=None): coins = self.wallet.get_spendable_coins(None) node_id = peer.pubkey @@ -1242,6 +1302,7 @@ class LNWallet(LNWorker): push_sat=push_sat, public=public, zeroconf=zeroconf, + opening_fee=opening_fee, password=password) return chan, funding_tx @@ -1254,6 +1315,7 @@ class LNWallet(LNWorker): push_sat: int, public: bool, zeroconf=False, + opening_fee=None, password: Optional[str]) -> Tuple[Channel, PartialTransaction]: coro = peer.channel_establishment_flow( @@ -1262,13 +1324,14 @@ class LNWallet(LNWorker): push_msat=push_sat * 1000, public=public, zeroconf=zeroconf, + opening_fee=opening_fee, temp_channel_id=os.urandom(32)) chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT) util.trigger_callback('channels_updated', self.wallet) self.wallet.adb.add_transaction(funding_tx) # save tx as local into the wallet self.wallet.sign_transaction(funding_tx, password) self.wallet.set_label(funding_tx.txid(), _('Open channel')) - if funding_tx.is_complete(): + if funding_tx.is_complete() and not zeroconf: await self.network.try_broadcasting(funding_tx, 'open_channel') return chan, funding_tx @@ -2412,11 +2475,20 @@ class LNWallet(LNWorker): def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None): """calculate routing hints (BOLT-11 'r' field)""" routing_hints = [] - if channels is None: - channels = list(self.get_channels_for_receiving(amount_msat)) - random.shuffle(channels) # let's not leak channel order - scid_to_my_channels = {chan.short_channel_id: chan for chan in channels - if chan.short_channel_id is not None} + if self.config.ZEROCONF_TRUSTED_NODE: + node_id, rest = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE) + alias_or_scid = self.get_scid_alias() + routing_hints.append(('r', [(node_id, alias_or_scid, 0, 0, 144)])) + # no need for more + channels = [] + else: + if channels is None: + channels = list(self.get_channels_for_receiving(amount_msat)) + random.shuffle(channels) # let's not leak channel order + scid_to_my_channels = { + chan.short_channel_id: chan for chan in channels + if chan.short_channel_id is not None + } for chan in channels: alias_or_scid = chan.get_remote_scid_alias() or chan.short_channel_id assert isinstance(alias_or_scid, bytes), alias_or_scid @@ -2758,6 +2830,8 @@ class LNWallet(LNWorker): await asyncio.sleep(1) if self.stopping_soon: return + if self.config.ZEROCONF_TRUSTED_NODE: + await self.add_peer(self.config.ZEROCONF_TRUSTED_NODE) for chan in self.channels.values(): if chan.is_closed(): continue diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 454198add..9d9c403be 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1169,9 +1169,10 @@ This will result in longer routes; it might increase your fees and decrease the SWAPSERVER_PORT = ConfigVar('swapserver_port', default=5455, type_=int) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) - # zeroconf + # zeroconf channels ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool) ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str) + ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar( diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index de6ea9351..2c66a6888 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -111,3 +111,39 @@ class TestLightningWatchtower(TestLightning): def test_watchtower(self): self.run_shell(['watchtower']) + + +class TestLightningJIT(TestLightning): + agents = { + 'alice':{ + 'accept_zeroconf_channels': 'true', + }, + 'bob':{ + 'lightning_listen': 'localhost:9735', + 'lightning_forward_payments': 'true', + 'accept_zeroconf_channels': 'true', + }, + 'carol':{ + } + } + + def test_just_in_time(self): + self.run_shell(['just_in_time']) + + +class TestLightningJITTrampoline(TestLightningJIT): + agents = { + 'alice':{ + 'use_gossip': 'false', + 'accept_zeroconf_channels': 'true', + }, + 'bob':{ + 'lightning_listen': 'localhost:9735', + 'lightning_forward_payments': 'true', + 'lightning_forward_trampoline_payments': 'true', + 'accept_zeroconf_channels': 'true', + }, + 'carol':{ + 'use_gossip': 'false', + } + } diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index bcce18464..76c06b5b6 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -440,6 +440,21 @@ if [[ $1 == "watchtower" ]]; then wait_until_spent $ctx_id 1 # alice's to_local gets punished immediately fi +if [[ $1 == "just_in_time" ]]; then + bob_node=$($bob nodeid) + $alice setconfig zeroconf_trusted_node $bob_node + $alice setconfig use_recoverable_channels false + wait_for_balance carol 1 + echo "carol opens channel with bob" + $carol open_channel $bob_node 0.15 --password='' + new_blocks 3 + wait_until_channel_open carol + echo "carol pays alice" + # note: set amount to 0.001 to test failure: 'payment too low' + invoice=$($alice add_request 0.01 -m "invoice" | jq -r ".lightning_invoice") + $carol lnpay $invoice +fi + if [[ $1 == "unixsockets" ]]; then # This looks different because it has to run the entire daemon # Test domain socket behavior From 7bebd62f13731e792f6290c09c25de197be810ac Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Nov 2023 09:23:17 +0100 Subject: [PATCH 3/3] zerofonf trusdted node: use NetworkRetryManager method _can_retry_addr --- electrum/lnutil.py | 6 ++++++ electrum/lnworker.py | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index eb34eefc7..04086fefa 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1484,6 +1484,12 @@ class LNPeerAddr: def __str__(self): return '{}@{}'.format(self.pubkey.hex(), self.net_addr_str()) + @classmethod + def from_str(cls, s): + node_id, rest = extract_nodeid(s) + host, port = split_host_port(rest) + return LNPeerAddr(host, int(port), node_id) + def __repr__(self): return f'' diff --git a/electrum/lnworker.py b/electrum/lnworker.py index dfedebf90..8a0582530 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2831,7 +2831,9 @@ class LNWallet(LNWorker): if self.stopping_soon: return if self.config.ZEROCONF_TRUSTED_NODE: - await self.add_peer(self.config.ZEROCONF_TRUSTED_NODE) + peer = LNPeerAddr.from_str(self.config.ZEROCONF_TRUSTED_NODE) + if self._can_retry_addr(peer, urgent=True): + await self._add_peer(peer.host, peer.port, peer.pubkey) for chan in self.channels.values(): if chan.is_closed(): continue