obelisk

Electrum server using libbitcoin as its backend
git clone https://git.parazyd.org/obelisk
Log | Files | Refs | README | LICENSE

commit f85302658f8923b7b7fa7e3e54b00670f99bf7b3
parent 31884907fcd4a75003e975d5d530e5790c65abdd
Author: parazyd <parazyd@dyne.org>
Date:   Wed,  7 Apr 2021 15:48:49 +0200

Initial async server implementation.

Diffstat:
Aelectrumobelisk/protocol.py | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aobelisk.py | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 320 insertions(+), 0 deletions(-)

diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py @@ -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 diff --git a/obelisk.py b/obelisk.py @@ -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())