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,