tremote watchtower: initial commit - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 680b129b4a92f619e6be85ce75901aef291eeff8 DIR parent 94a10e6307c25499f4fd556987f8d4d0c2e14b53 HTML Author: ThomasV <thomasv@electrum.org> Date: Fri, 12 Oct 2018 14:53:22 +0200 remote watchtower: initial commit Diffstat: M electrum/daemon.py | 15 ++++++++++++--- M electrum/jsonrpc.py | 37 ++++++++++++++++++++++++++----- M electrum/lnwatcher.py | 24 +++++++++++++++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) --- DIR diff --git a/electrum/daemon.py b/electrum/daemon.py t@@ -33,7 +33,7 @@ from typing import Dict, Optional, Tuple import jsonrpclib -from .jsonrpc import VerifyingJSONRPCServer +from .jsonrpc import SimpleJSONRPCServer, PasswordProtectedJSONRPCServer from .version import ELECTRUM_VERSION from .network import Network from .util import (json_decode, DaemonThread, to_string, t@@ -147,6 +147,8 @@ class Daemon(DaemonThread): self.server = None if listen_jsonrpc: self.init_server(config, fd) + if config.get('watchtower_host'): + self.init_watchtower() self.start() def init_server(self, config: SimpleConfig, fd): t@@ -154,8 +156,9 @@ class Daemon(DaemonThread): port = config.get('rpcport', 0) rpc_user, rpc_password = get_rpc_credentials(config) try: - server = VerifyingJSONRPCServer((host, port), logRequests=False, - rpc_user=rpc_user, rpc_password=rpc_password) + server = PasswordProtectedJSONRPCServer( + (host, port), logRequests=False, + rpc_user=rpc_user, rpc_password=rpc_password) except Exception as e: self.logger.error(f'cannot initialize RPC server on host {host}: {repr(e)}') self.server = None t@@ -173,6 +176,12 @@ class Daemon(DaemonThread): server.register_function(getattr(self.cmd_runner, cmdname), cmdname) server.register_function(self.run_cmdline, 'run_cmdline') + def init_watchtower(self): + host = self.config.get('watchtower_host') + port = self.config.get('watchtower_port', 12345) + server = SimpleJSONRPCServer((host, port), logRequests=False) + server.register_function(self.network.lnwatcher, 'add_sweep_tx') + def ping(self): return True DIR diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py t@@ -1,3 +1,4 @@ + #!/usr/bin/env python3 # # Electrum - lightweight Bitcoin client t@@ -48,12 +49,10 @@ class RPCAuthUnsupportedType(Exception): # based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke -class VerifyingJSONRPCServer(SimpleJSONRPCServer, Logger): +class AuthenticatedJSONRPCServer(SimpleJSONRPCServer, Logger): - def __init__(self, *args, rpc_user, rpc_password, **kargs): + def __init__(self, *args, **kargs): Logger.__init__(self) - self.rpc_user = rpc_user - self.rpc_password = rpc_password class VerifyingRequestHandler(SimpleJSONRPCRequestHandler): def parse_request(myself): t@@ -73,11 +72,21 @@ class VerifyingJSONRPCServer(SimpleJSONRPCServer, Logger): self.logger.exception('') myself.send_error(500, repr(e)) return False - SimpleJSONRPCServer.__init__( self, requestHandler=VerifyingRequestHandler, *args, **kargs) def authenticate(self, headers): + raise Exception('undefined') + + +class PasswordProtectedJSONRPCServer(AuthenticatedJSONRPCServer): + + def __init__(self, *args, rpc_user, rpc_password, **kargs): + self.rpc_user = rpc_user + self.rpc_password = rpc_password + AuthenticatedJSONRPCServer.__init__(self, *args, **kargs) + + def authenticate(self, headers): if self.rpc_password == '': # RPC authentication is disabled return t@@ -97,3 +106,21 @@ class VerifyingJSONRPCServer(SimpleJSONRPCServer, Logger): and util.constant_time_compare(password, self.rpc_password)): time.sleep(0.050) raise RPCAuthCredentialsInvalid() + + +class AccountsJSONRPCServer(AuthenticatedJSONRPCServer): + """ user accounts """ + + def __init__(self, *args, **kargs): + self.users = {} + AuthenticatedJSONRPCServer.__init__(self, *args, **kargs) + self.register_function(self.add_user, 'add_user') + + def authenticate(self, headers): + # todo: verify signature + return + + def add_user(self, pubkey): + user_id = len(self.users) + self.users[user_id] = pubkey + return user_id DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py t@@ -2,6 +2,8 @@ import threading from typing import NamedTuple, Iterable import os from collections import defaultdict +import asyncio +import jsonrpclib from .util import PrintError, bh2u, bfh, NoDynamicFeeEstimates, aiosafe from .lnutil import EncumberedTransaction, Outpoint t@@ -20,7 +22,7 @@ class LNWatcher(PrintError): def __init__(self, network): self.network = network - + self.config = network.config path = os.path.join(network.config.path, "watcher_db") storage = WalletStorage(path) self.addr_sync = AddressSynchronizer(storage) t@@ -41,6 +43,25 @@ class LNWatcher(PrintError): self.network.register_callback(self.on_network_update, ['network_updated', 'blockchain_updated', 'verified', 'wallet_updated']) + # remote watchtower + watchtower_url = self.config.get('watchtower_url') + self.watchtower = jsonrpclib.Server(watchtower_url) if watchtower_url else None + self.watchtower_queue = asyncio.Queue() + asyncio.run_coroutine_threadsafe(self.watchtower_task(), self.network.asyncio_loop) + + def with_watchtower(func): + def wrapper(self, *args, **kwargs): + if self.watchtower: + self.watchtower_queue.put_nowait((func.__name__, args, kwargs)) + return func(self, *args, **kwargs) + return wrapper + + async def watchtower_task(self): + while True: + name, args, kwargs = await self.watchtower_queue.get() + self.print_error('sending to watchtower', name, args) + func = getattr(self.watchtower, name) + func(*args, **kwargs) def write_to_disk(self): # FIXME: json => every update takes linear instead of constant disk write t@@ -151,6 +172,7 @@ class LNWatcher(PrintError): .format(num_conf, e_tx.csv_delay, funding_outpoint, ctx.txid())) return keep_watching_this + @with_watchtower def add_sweep_tx(self, funding_outpoint: str, ctx_txid: str, encumbered_sweeptx: EncumberedTransaction): if encumbered_sweeptx is None: return