URI: 
       tInitial async server implementation. - obelisk - Electrum server using libbitcoin as its backend
  HTML git clone https://git.parazyd.org/obelisk
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit f85302658f8923b7b7fa7e3e54b00670f99bf7b3
   DIR parent 31884907fcd4a75003e975d5d530e5790c65abdd
  HTML Author: parazyd <parazyd@dyne.org>
       Date:   Wed,  7 Apr 2021 15:48:49 +0200
       
       Initial async server implementation.
       
       Diffstat:
         A electrumobelisk/protocol.py         |     180 +++++++++++++++++++++++++++++++
         A obelisk.py                          |     140 +++++++++++++++++++++++++++++++
       
       2 files changed, 320 insertions(+), 0 deletions(-)
       ---
   DIR diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py
       t@@ -0,0 +1,180 @@
       +#!/usr/bin/env python3
       +# Copyright (C) 2020-2021 Ivan J. <parazyd@dyne.org>
       +#
       +# This file is part of obelisk
       +#
       +# This program is free software: you can redistribute it and/or modify
       +# it under the terms of the GNU Affero General Public License version 3
       +# as published by the Free Software Foundation.
       +#
       +# This program is distributed in the hope that it will be useful,
       +# but WITHOUT ANY WARRANTY; without even the implied warranty of
       +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       +# GNU Affero General Public License for more details.
       +#
       +# You should have received a copy of the GNU Affero General Public License
       +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
       +import asyncio
       +import json
       +
       +VERSION = 0.0
       +DONATION_ADDR = "bc1q7an9p5pz6pjwjk4r48zke2yfaevafzpglg26mz"
       +
       +BANNER = (
       +    """
       +Welcome to obelisk
       +
       +"Tools for the people"
       +
       +obelisk is a server that uses libbitcoin-server as its backend.
       +Source code can be found at: https://github.com/parazyd/obelisk
       +
       +Please consider donating: %s
       +"""
       +    % DONATION_ADDR
       +)
       +
       +
       +class ElectrumProtocol(asyncio.Protocol):
       +    """TBD"""
       +
       +    def __init__(self, log, chain, endpoints, server_cfg):
       +        self.log = log
       +        self.endpoints = endpoints
       +        self.server_cfg = server_cfg
       +
       +        if chain == "mainnet":
       +            self.genesis = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
       +        elif chain == "testnet":
       +            self.genesis = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
       +        else:
       +            raise ValueError(f"Invalid chain '{chain}'")
       +
       +    async def recv(self, reader, writer):
       +        recv_buf = bytearray()
       +        while True:
       +            data = await reader.read(4096)
       +            if not data or len(data) == 0:
       +                self.log.debug("Received EOF, disconnect")
       +                return
       +            recv_buf.extend(data)
       +            lb = recv_buf.find(b"\n")
       +            if lb == -1:
       +                continue
       +            while lb != -1:
       +                line = recv_buf[:lb].rstrip()
       +                recv_buf = recv_buf[lb + 1 :]
       +                lb = recv_buf.find(b"\n")
       +                try:
       +                    line = line.decode("utf-8")
       +                    query = json.loads(line)
       +                except (UnicodeDecodeError, json.JSONDecodeError) as err:
       +                    self.log.debug("Got error: %s", repr(err))
       +                    break
       +                self.log.debug("=> " + line)
       +                self.handle_query(writer, query)
       +
       +    async def handle_query(
       +        self, writer, query
       +    ):  # pylint: disable=R0915,R0912,R0911
       +        """Electrum protocol method handlers"""
       +        # https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html
       +        if "method" not in query:
       +            self.log.debug("No 'method' in query: %s", query)
       +
       +        method = query["method"]
       +
       +        if method == "blockchain.block.header":
       +            self.log.debug("blockchain.block.header")
       +            return
       +
       +        if method == "blockchain.block.headers":
       +            self.log.debug("blockchain.block.headers")
       +            return
       +
       +        if method == "blockchain.estimatefee":
       +            self.log.debug("blockchain.estimatefee")
       +            return
       +
       +        if method == "blockchain.headers.subscribe":
       +            self.log.debug("blockchain.headers.subscribe")
       +            return
       +
       +        if method == "blockchain.relayfee":
       +            self.log.debug("blockchain.relayfee")
       +            return
       +
       +        if method == "blockchain.scripthash.get_balance":
       +            self.log.debug("blockchain.scripthash.get_balance")
       +            return
       +
       +        if method == "blockchain.scripthash.get_history":
       +            self.log.debug("blockchain.scripthash.get_history")
       +            return
       +
       +        if method == "blockchain.scripthash.get_mempool":
       +            self.log.debug("blockchain.scripthash.get_mempool")
       +            return
       +
       +        if method == "blockchain.scripthash.listunspent":
       +            self.log.debug("blockchain.scripthash.listunspent")
       +            return
       +
       +        if method == "blockchain.scripthash.subscribe":
       +            self.log.debug("blockchain.scripthash.subscribe")
       +            return
       +
       +        if method == "blockchain.scripthash.unsubscribe":
       +            self.log.debug("blockchain.scripthash.unsubscribe")
       +            return
       +
       +        if method == "blockchain.transaction.broadcast":
       +            self.log.debug("blockchain.transaction.broadcast")
       +            return
       +
       +        if method == "blockchain.transaction.get":
       +            self.log.debug("blockchain.transaction.get")
       +            return
       +
       +        if method == "blockchain.transaction.get_merkle":
       +            self.log.debug("blockchain.transaction.get_merkle")
       +            return
       +
       +        if method == "blockchain.transaction.id_from_pos":
       +            self.log.debug("blockchain.transaction.id_from_pos")
       +            return
       +
       +        if method == "mempool.get_fee_histogram":
       +            self.log.debug("mempool.get_fee_histogram")
       +            return
       +
       +        if method == "server.add_peer":
       +            self.log.debug("server.add_peer")
       +            return
       +
       +        if method == "server.banner":
       +            self.log.debug("server.banner")
       +            return
       +
       +        if method == "server.donation_address":
       +            self.log.debug("server.donation_address")
       +            return
       +
       +        if method == "server.features":
       +            self.log.debug("server.features")
       +            return
       +
       +        if method == "server.peers.subscribe":
       +            self.log.debug("server.peers.subscribe")
       +            return
       +
       +        if method == "server.ping":
       +            self.log.debug("server.ping")
       +            return
       +
       +        if method == "server.version":
       +            self.log.debug("server.version")
       +            return
       +
       +        self.log.error("BUG? Unhandled method: '%s' query=%s", method, query)
       +        return
   DIR diff --git a/obelisk.py b/obelisk.py
       t@@ -0,0 +1,140 @@
       +#!/usr/bin/env python3
       +# Copyright (C) 2020-2021 Ivan J. <parazyd@dyne.org>
       +#
       +# This file is part of obelisk
       +#
       +# This program is free software: you can redistribute it and/or modify
       +# it under the terms of the GNU Affero General Public License version 3
       +# as published by the Free Software Foundation.
       +#
       +# This program is distributed in the hope that it will be useful,
       +# but WITHOUT ANY WARRANTY; without even the implied warranty of
       +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       +# GNU Affero General Public License for more details.
       +#
       +# You should have received a copy of the GNU Affero General Public License
       +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
       +import asyncio
       +import sys
       +from argparse import ArgumentParser
       +from configparser import RawConfigParser, NoSectionError
       +from logging import getLogger, FileHandler, Formatter, StreamHandler, DEBUG
       +from os.path import exists, join
       +from tempfile import gettempdir
       +
       +from pkg_resources import resource_filename
       +
       +from electrumobelisk.protocol import ElectrumProtocol, VERSION
       +
       +
       +def logger_config(log, config):
       +    """Setup logging"""
       +    fmt = Formatter(
       +        config.get(
       +            "obelisk",
       +            "log_format",
       +            fallback="%(asctime)s\t%(levelname)s\t%(message)s",
       +        )
       +    )
       +    logstream = StreamHandler()
       +    logstream.setFormatter(fmt)
       +    logstream.setLevel(
       +        config.get("obelisk", "log_level_stdout", fallback="DEBUG")
       +    )
       +    log.addHandler(logstream)
       +    filename = config.get("obelisk", "log_file_location", fallback="")
       +    if len(filename.strip()) == 0:
       +        filename = join(gettempdir(), "obelisk.log")
       +    logfile = FileHandler(
       +        filename,
       +        mode=(
       +            "a"
       +            if config.get("obelisk", "append_log", fallback="false")
       +            else "w"
       +        ),
       +    )
       +    logfile.setFormatter(fmt)
       +    logfile.setLevel(DEBUG)
       +    log.addHandler(logfile)
       +    log.setLevel(DEBUG)
       +    return log, filename
       +
       +
       +def get_certs(config):
       +    """Get file paths to TLS cert and key"""
       +    certfile = config.get("obelisk", "certfile", fallback=None)
       +    keyfile = config.get("obelisk", "keyfile", fallback=None)
       +    if (certfile and keyfile) and (exists(certfile) and exists(keyfile)):
       +        return certfile, keyfile
       +
       +    certfile = resource_filename("electrumobelisk", "certs/cert.pem")
       +    keyfile = resource_filename("electrumobelisk", "certs/cert.key")
       +    if exists(certfile) and exists(keyfile):
       +        return certfile, keyfile
       +
       +    raise ValueError(f"TLS keypair not found ({certfile}, {keyfile})")
       +
       +
       +async def run_electrum_server(config, chain):
       +    """Server coroutine"""
       +    log = getLogger("obelisk")
       +    host = config.get("obelisk", "host")
       +    port = int(config.get("obelisk", "port"))
       +
       +    if config.getboolean("obelisk", "usetls", fallback=True):
       +        certfile, keyfile = get_certs(config)
       +        log.debug("Using TLS with keypair: %s , %s", certfile, keyfile)
       +
       +    broadcast_method = config.get(
       +        "obelisk", "broadcast_method", fallback="tor"
       +    )
       +    tor_host = config.get("obelisk", "tor_host", fallback="localhost")
       +    tor_port = int(config.get("obelisk", "tor_port", fallback=9050))
       +
       +    endpoints = {}
       +    endpoints["query"] = config.get("obelisk", "query")
       +    endpoints["heart"] = config.get("obelisk", "heart")
       +    endpoints["block"] = config.get("obelisk", "block")
       +    endpoints["trans"] = config.get("obelisk", "trans")
       +
       +    server_cfg = {}
       +    server_cfg["torhostport"] = (tor_host, tor_port)
       +    server_cfg["broadcast_method"] = broadcast_method
       +
       +    protocol = ElectrumProtocol(log, chain, endpoints, server_cfg)
       +
       +    server = await asyncio.start_server(protocol.recv, host, port)
       +    async with server:
       +        await server.serve_forever()
       +
       +
       +def main():
       +    """Main orchestration"""
       +    parser = ArgumentParser(description=f"obelisk {VERSION}")
       +    parser.add_argument("config_file", help="Path to config file")
       +    args = parser.parse_args()
       +
       +    try:
       +        config = RawConfigParser()
       +        config.read(args.config_file)
       +        config.options("obelisk")
       +    except NoSectionError:
       +        print(f"error: Invalid config file {args.config_file}")
       +        return 1
       +
       +    log = getLogger("obelisk")
       +    log, logfilename = logger_config(log, config)
       +    log.info(f"Starting obelisk {VERSION}")
       +    log.info(f"Logging to {logfilename}")
       +
       +    chain = config.get("obelisk", "chain")
       +    if chain not in ("mainnet", "testnet"):
       +        log.error("chain is not 'mainnet' or 'testnet'")
       +        return 1
       +
       +    asyncio.run(run_electrum_server(config, chain))
       +    return 1
       +
       +
       +if __name__ == "__main__":
       +    sys.exit(main())