commit f85302658f8923b7b7fa7e3e54b00670f99bf7b3
parent 31884907fcd4a75003e975d5d530e5790c65abdd
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(-)
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())