Browse Source

Merge pull request #8671 from spesmilo/just_in_time_channels

Just in time channels
master
ThomasV 2 years ago committed by GitHub
parent
commit
6865204d7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      electrum/commands.py
  2. 12
      electrum/lnchannel.py
  3. 125
      electrum/lnpeer.py
  4. 13
      electrum/lnutil.py
  5. 2
      electrum/lnwire/peer_wire.csv
  6. 101
      electrum/lnworker.py
  7. 5
      electrum/simple_config.py
  8. 36
      electrum/tests/regtest.py
  9. 15
      electrum/tests/regtest/regtest.sh

4
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"),

12
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"])
@ -695,6 +696,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 +835,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 +1704,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:

125
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!"
@ -627,6 +629,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,7 +715,9 @@ class Peer(Logger):
funding_sat: int,
push_msat: int,
public: bool,
temp_channel_id: bytes
zeroconf: bool = False,
temp_channel_id: bytes,
opening_fee: int = None,
) -> Tuple[Channel, 'PartialTransaction']:
"""Implements the channel opening flow.
@ -736,6 +743,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.
@ -751,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,
@ -791,7 +804,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 +916,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
@ -954,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:
@ -1009,7 +1030,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
@ -1059,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):
@ -1078,6 +1105,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 +1451,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()})")
@ -1697,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}")
@ -1737,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()})")
@ -1820,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,
@ -1909,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(
@ -1989,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
@ -2001,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
@ -2569,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

13
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
@ -1477,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'<LNPeerAddr host={self.host} port={self.port} pubkey={self.pubkey.hex()}>'

2
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,

1 msgtype,init,16
61 tlvdata,open_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,...
62 tlvtype,open_channel_tlvs,channel_type,1
63 tlvdata,open_channel_tlvs,channel_type,type,byte,...
64 tlvtype,open_channel_tlvs,channel_opening_fee,10000
65 tlvdata,open_channel_tlvs,channel_opening_fee,channel_opening_fee,u64,
66 msgtype,accept_channel,33
67 msgdata,accept_channel,temporary_channel_id,byte,32
68 msgdata,accept_channel,dust_limit_satoshis,u64,

101
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
@ -1218,11 +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
@ -1237,6 +1301,8 @@ class LNWallet(LNWorker):
funding_sat=funding_sat,
push_sat=push_sat,
public=public,
zeroconf=zeroconf,
opening_fee=opening_fee,
password=password)
return chan, funding_tx
@ -1248,6 +1314,8 @@ class LNWallet(LNWorker):
funding_sat: int,
push_sat: int,
public: bool,
zeroconf=False,
opening_fee=None,
password: Optional[str]) -> Tuple[Channel, PartialTransaction]:
coro = peer.channel_establishment_flow(
@ -1255,13 +1323,15 @@ class LNWallet(LNWorker):
funding_sat=funding_sat,
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
@ -2306,7 +2376,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
@ -2405,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
@ -2751,6 +2830,10 @@ class LNWallet(LNWorker):
await asyncio.sleep(1)
if self.stopping_soon:
return
if 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

5
electrum/simple_config.py

@ -1169,6 +1169,11 @@ 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 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(
'use_watchtower', default=False, type_=bool,

36
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',
}
}

15
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

Loading…
Cancel
Save