Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bolt8 transport for electrum protocol #8049

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,14 @@ async def setconfig(self, key, value):
self.config.set_key(key, value)
return True

@command('')
async def create_key_for_server(self, server_pubkey:str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this name confusing. Both the client and the server ultimately has keypair(s). So which one is create_key_for_server creating a keypair for?

How about create_userkey_for_server?

" returns a pubkey to add to electrumx, using the 'add_user' RPC"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add import/export functionality? I guess it would be easier and cleaner not to, but tell users instead to generate new keys instead. Maybe this could be mentioned in the docstring.

However, in time I expect users will ask for it. E.g. consider operating a server for your friends: generating a new key requires user-interaction/cooperation. Anyway, I guess it's easier to ignore this for now.

privkey = os.urandom(32)
pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=True)
self.config.set_bolt8_privkey_for_server(server_pubkey, privkey.hex())
return pubkey.hex()

@command('')
async def get_ssl_domain(self):
"""Check and return the SSL domain set in ssl_keyfile and ssl_certfile
Expand Down Expand Up @@ -1241,8 +1249,8 @@ async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: A

@command('wnl')
async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):
""" return the local watchtower's ctn of channel. used in regtests """
return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None)
""" return the remote watchtower's ctn of channel. used in regtests """
return await self.network.watchtower_get_ctn(channel_point, None)

@command('wnl')
async def rebalance_channels(self, from_scid, dest_scid, amount, wallet: Abstract_Wallet = None):
Expand Down
34 changes: 0 additions & 34 deletions electrum/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,34 +340,6 @@ async def run_cmdline(self, config_options):
return result


class WatchTowerServer(AuthenticatedServer):

def __init__(self, network, netaddress):
self.addr = netaddress
self.config = network.config
self.network = network
watchtower_user = self.config.get('watchtower_user', '')
watchtower_password = self.config.get('watchtower_password', '')
AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
self.lnwatcher = network.local_watchtower
self.app = web.Application()
self.app.router.add_post("/", self.handle)
self.register_method(self.get_ctn)
self.register_method(self.add_sweep_tx)

async def run(self):
self.runner = web.AppRunner(self.app)
await self.runner.setup()
site = web.TCPSite(self.runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context())
await site.start()
self.logger.info(f"now running and listening. addr={self.addr}")

async def get_ctn(self, *args):
return await self.lnwatcher.get_ctn(*args)

async def add_sweep_tx(self, *args):
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)




Expand Down Expand Up @@ -403,12 +375,6 @@ def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
if listen_jsonrpc:
self.commands_server = CommandsServer(self, fd)
daemon_jobs.append(self.commands_server.run())
# server-side watchtower
self.watchtower = None
watchtower_address = self.config.get_netaddress('watchtower_address')
if not config.get('offline') and watchtower_address:
self.watchtower = WatchTowerServer(self.network, watchtower_address)
daemon_jobs.append(self.watchtower.run)
if self.network:
self.network.start(jobs=[self.fx.run])
# prepare lightning functionality, also load channel db early
Expand Down
7 changes: 0 additions & 7 deletions electrum/gui/qt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,6 @@ def build_tray_menu(self):
m.addAction(_("Network"), self.show_network_dialog)
if network and network.lngossip:
m.addAction(_("Lightning Network"), self.show_lightning_dialog)
if network and network.local_watchtower:
m.addAction(_("Local Watchtower"), self.show_watchtower_dialog)
for window in self.windows:
name = window.wallet.basename()
submenu = m.addMenu(name)
Expand Down Expand Up @@ -285,11 +283,6 @@ def show_lightning_dialog(self):
self.lightning_dialog = LightningDialog(self)
self.lightning_dialog.bring_to_top()

def show_watchtower_dialog(self):
if not self.watchtower_dialog:
self.watchtower_dialog = WatchtowerDialog(self)
self.watchtower_dialog.bring_to_top()

def show_network_dialog(self):
if self.network_dialog:
self.network_dialog.on_event_network_updated()
Expand Down
2 changes: 0 additions & 2 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,6 @@ def add_toggle_action(view_menu, tab):
tools_menu.addAction(_("Electrum preferences"), self.settings_dialog)

tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network))
if self.network and self.network.local_watchtower:
tools_menu.addAction(_("Local &Watchtower"), self.gui_object.show_watchtower_dialog)
tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
tools_menu.addSeparator()
tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
Expand Down
40 changes: 33 additions & 7 deletions electrum/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
from .i18n import _
from .logging import Logger
from .transaction import Transaction
from .lnutil import LNPeerAddr
from .lntransport import LNClient

if TYPE_CHECKING:
from .network import Network
Expand All @@ -70,7 +72,7 @@

MAX_INCOMING_MSG_SIZE = 1_000_000 # in bytes

_KNOWN_NETWORK_PROTOCOLS = {'t', 's'}
_KNOWN_NETWORK_PROTOCOLS = {'t', 's', 'b'}
PREFERRED_NETWORK_PROTOCOL = 's'
assert PREFERRED_NETWORK_PROTOCOL in _KNOWN_NETWORK_PROTOCOLS

Expand Down Expand Up @@ -271,7 +273,7 @@ async def create_connection(self):

class ServerAddr:

def __init__(self, host: str, port: Union[int, str], *, protocol: str = None):
def __init__(self, host: str, port: Union[int, str], *, protocol: str = None, pubkey: str = None):
assert isinstance(host, str), repr(host)
if protocol is None:
protocol = 's'
Expand All @@ -288,13 +290,20 @@ def __init__(self, host: str, port: Union[int, str], *, protocol: str = None):
self.host = str(net_addr.host) # canonical form (if e.g. IPv6 address)
self.port = int(net_addr.port)
self.protocol = protocol
self.pubkey = pubkey
self._net_addr_str = str(net_addr)

@classmethod
def from_str(cls, s: str) -> 'ServerAddr':
# host might be IPv6 address, hence do rsplit:
host, port, protocol = str(s).rsplit(':', 2)
return ServerAddr(host=host, port=port, protocol=protocol)
s = str(s).rsplit(':', 3)
if len(s) == 4:
host, port, protocol, pubkey = s
assert protocol == 'b'
elif len(s) == 3:
host, port, protocol = s
pubkey = None
return ServerAddr(host=host, port=port, protocol=protocol, pubkey=pubkey)

@classmethod
def from_str_with_inference(cls, s: str) -> Optional['ServerAddr']:
Expand Down Expand Up @@ -654,11 +663,28 @@ def is_main_server(self) -> bool:
return (self.network.interface == self or
self.network.interface is None and self.network.default_server == self.server)

#@log_exceptions
async def open_session(self, sslc, exit_early=False):
session_factory = lambda *args, iface=self, **kwargs: NotificationSession(*args, **kwargs, interface=iface)
async with _RSClient(session_factory=session_factory,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy) as session:

def create_client():
if self.protocol == 'b':
peer_addr = LNPeerAddr(self.host, self.port, bytes.fromhex(self.server.pubkey))
privkey = self.network.config.get_bolt8_privkey_for_server(self.server.pubkey)
privkey = bytes.fromhex(privkey) if privkey else os.urandom(32)
return LNClient(
prologue=b'electrum',
privkey=privkey,
session_factory=session_factory,
peer_addr=peer_addr,
proxy=self.proxy)
else:
return _RSClient(
session_factory=session_factory,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy)

async with create_client() as session:
self.session = session # type: NotificationSession
self.session.set_default_timeout(self.network.get_network_timeout_seconds(NetworkTimeout.Generic))
try:
Expand Down
15 changes: 6 additions & 9 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
ChannelType, LNProtocolWarning)
from .lnutil import FeeUpdate, channel_id_from_funding_tx
from .lntransport import LNTransport, LNTransportBase
from .lntransport import LNTransport
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType
from .interface import GracefulDisconnect
from .lnrouter import fee_for_edge_msat
Expand Down Expand Up @@ -76,7 +76,7 @@ def __init__(
self,
lnworker: Union['LNGossip', 'LNWallet'],
pubkey: bytes,
transport: LNTransportBase,
transport: LNTransport,
*, is_channel_backup= False):

self.lnworker = lnworker
Expand All @@ -90,7 +90,7 @@ def __init__(
self.querying = asyncio.Event()
self.transport = transport
self.pubkey = pubkey # remote pubkey
self.privkey = self.transport.privkey # local privkey
self.privkey = self.transport._privkey # local privkey
self.features = self.lnworker.features # type: LnFeatures
self.their_features = LnFeatures(0) # type: LnFeatures
self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
Expand Down Expand Up @@ -155,10 +155,7 @@ def is_initialized(self) -> bool:
and self.initialized.result() is True)

async def initialize(self):
# If outgoing transport, do handshake now. For incoming, it has already been done.
if isinstance(self.transport, LNTransport):
await self.transport.handshake()
self.logger.info(f"handshake done for {self.transport.peer_addr or self.pubkey.hex()}")
assert self.transport.handshake_done.is_set()
features = self.features.for_init_message()
b = int.bit_length(features)
flen = b // 8 + int(bool(b % 8))
Expand Down Expand Up @@ -847,7 +844,7 @@ async def channel_establishment_flow(
)
chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]
chan.storage['has_onchain_backup'] = has_onchain_backup
if isinstance(self.transport, LNTransport):
if not self.transport.is_listener():
chan.add_or_update_peer_addr(self.transport.peer_addr)
sig_64, _ = chan.sign_next_commitment()
self.temp_id_to_id[temp_channel_id] = channel_id
Expand Down Expand Up @@ -1024,7 +1021,7 @@ async def on_open_channel(self, payload):
initial_feerate=feerate
)
chan.storage['init_timestamp'] = int(time.time())
if isinstance(self.transport, LNTransport):
if not self.transport.is_listener():
chan.add_or_update_peer_addr(self.transport.peer_addr)
remote_sig = funding_created['signature']
try:
Expand Down
Loading