├── obelisk ├── __init__.py ├── mempool_api.py ├── errors_jsonrpc.py ├── merkle.py ├── util.py ├── obelisk ├── errors_libbitcoin.py ├── zeromq.py └── protocol.py ├── run_obelisk ├── res ├── obelisk.png └── obelisk.cfg ├── .gitignore ├── tests ├── __main__.py └── test_electrum_protocol.py ├── Makefile ├── setup.py ├── .github └── workflows │ ├── py.yaml │ └── codeql-analysis.yml ├── README.md └── LICENSE /obelisk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run_obelisk: -------------------------------------------------------------------------------- 1 | obelisk/obelisk -------------------------------------------------------------------------------- /res/obelisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parazyd/obelisk/HEAD/res/obelisk.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .coverage 3 | coverage.xml 4 | htmlcov 5 | build 6 | dist 7 | *.egg-info 8 | *.swp 9 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from test_electrum_protocol import main 3 | 4 | asyncio.run(main()) 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo "Available targets:" 3 | @echo 4 | @echo "make coverage - Run test units and report code coverage" 5 | @echo "make test - Run test units" 6 | @echo "make format - Format code" 7 | 8 | format: 9 | yapf --style google -i -r . 10 | 11 | test: 12 | python3 tests 13 | 14 | coverage: 15 | coverage run tests 16 | coverage report 17 | coverage html 18 | coverage xml 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | from obelisk.protocol import VERSION 5 | 6 | setup( 7 | name="obelisk", 8 | version=VERSION, 9 | scripts=["obelisk/obelisk"], 10 | python_requires=">=3.7", 11 | install_requires=["pyzmq"], 12 | packages=["obelisk"], 13 | description="Obelisk Electrum server", 14 | author="Ivan J.", 15 | author_email="parazyd@dyne.org", 16 | license="AGPL-3", 17 | url="https://github.com/parazyd/obelisk", 18 | long_description="Electrum server using libbitcoin and zmq as backend", 19 | include_package_data=True, 20 | data_files=[("share/doc/obelisk", ["README.md", "res/obelisk.cfg"])], 21 | ) 22 | -------------------------------------------------------------------------------- /res/obelisk.cfg: -------------------------------------------------------------------------------- 1 | # Configuration file for obelisk 2 | [obelisk] 3 | # 0.0.0.0 to bind to any IP, 127.0.0.1 to bind to localhost 4 | host = 127.0.0.1 5 | port = 50003 6 | # Public hostname for the server, ideally a valid domain or reachable IP 7 | # (comma-separated if there are more, e.g. Tor hidden service) 8 | hostname = localhost 9 | 10 | # Which chain should this instance of obelisk be using (mainnet/testnet) 11 | chain = testnet 12 | 13 | # Endpoints served by bs 14 | query = tcp://testnet2.libbitcoin.net:29091 15 | heart = tcp://testnet2.libbitcoin.net:29092 16 | block = tcp://testnet2.libbitcoin.net:29093 17 | trans = tcp://testnet2.libbitcoin.net:29094 18 | 19 | # Log level (INFO/WARNING/DEBUG) 20 | log_level = DEBUG 21 | 22 | # Path to log file for writing 23 | #log_file = obelisk.log 24 | 25 | # Append to logfile 26 | #append_log = true 27 | -------------------------------------------------------------------------------- /.github/workflows/py.yaml: -------------------------------------------------------------------------------- 1 | name: Python tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.7, 3.8, 3.9] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pyzmq 22 | pip install coverage 23 | pip install -e . 24 | - name: Run tests and make coverage report 25 | run: | 26 | make coverage 27 | - name: Upload coverage report 28 | uses: codecov/codecov-action@v1 29 | with: 30 | files: ./coverage.xml 31 | flags: unittests 32 | env_vars: OS,PYTHON 33 | fail_ci_if_error: true 34 | verbose: true 35 | -------------------------------------------------------------------------------- /obelisk/mempool_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2020-2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """mempool.space API functions""" 18 | import json 19 | from http.client import HTTPSConnection 20 | 21 | 22 | def get_mempool_fee_estimates(): 23 | conn = HTTPSConnection("mempool.space") 24 | conn.request("GET", "/api/v1/fees/recommended") 25 | res = conn.getresponse() 26 | 27 | if res.status != 200: 28 | return None 29 | 30 | return json.load(res) 31 | 32 | 33 | def get_fee_histogram(): 34 | conn = HTTPSConnection("mempool.space") 35 | conn.request("GET", "/api/mempool") 36 | res = conn.getresponse() 37 | 38 | if res.status != 200: 39 | return None 40 | 41 | return json.load(res)["fee_histogram"] 42 | -------------------------------------------------------------------------------- /obelisk/errors_jsonrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """JSON-RPC errors: https://www.jsonrpc.org/specification#error_object""" 18 | 19 | 20 | class JsonRPCError: # pragma: no cover 21 | """Class implementing functions returning JSON-RPC errors""" 22 | 23 | def __init__(self): 24 | return 25 | 26 | @staticmethod 27 | def invalidrequest(): 28 | return {"error": {"code": -32600, "message": "invalid request"}} 29 | 30 | @staticmethod 31 | def methodnotfound(): 32 | return {"error": {"code": -32601, "message": "method not found"}} 33 | 34 | @staticmethod 35 | def invalidparams(): 36 | return {"error": {"code": -32602, "message": "invalid parameters"}} 37 | 38 | @staticmethod 39 | def internalerror(): 40 | return {"error": {"code": -32603, "message": "internal error"}} 41 | 42 | @staticmethod 43 | def parseerror(): 44 | return {"error": {"code": -37200, "message": "parse error"}} 45 | 46 | @staticmethod 47 | def protonotsupported(): 48 | return { 49 | "error": { 50 | "code": -32100, 51 | "message": "protocol version unsupported", 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | obelisk 2 | ======= 3 | 4 | ![obelisk](res/obelisk.png) 5 | 6 | Python implementation of an [Electrum](https://electrum.org) server 7 | using [libbitcoin](https://libbitcoin.info) as a backend. 8 | 9 | [![Tests](https://github.com/parazyd/obelisk/actions/workflows/py.yaml/badge.svg)](https://github.com/parazyd/obelisk/actions/workflows/py.yaml) 10 | [![CodeQL](https://github.com/parazyd/obelisk/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/parazyd/obelisk/actions/workflows/codeql-analysis.yml) 11 | [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE) 12 | [![codecov](https://codecov.io/gh/parazyd/obelisk/branch/master/graph/badge.svg?token=JL5FKYM9IX)](https://codecov.io/gh/parazyd/obelisk) 13 | 14 | Please consider donating to support development: 15 | 16 | ``` 17 | bc1q7an9p5pz6pjwjk4r48zke2yfaevafzpglg26mz 18 | ``` 19 | 20 | 21 | TODO 22 | ---- 23 | 24 | * git grep -nE "TODO:|BUG:" 25 | 26 | 27 | Dependencies 28 | ------------ 29 | 30 | * Python 3.7 or later 31 | * [pyzmq](https://pypi.org/project/pyzmq/) (python3-zmq or dev-python/pyzmq) 32 | * [libbitcoin-server](https://github.com/libbitcoin/libbitcoin-server) (optional) 33 | 34 | 35 | Usage 36 | ----- 37 | 38 | Set up [obelisk.cfg](res/obelisk.cfg), and run 39 | 40 | ``` 41 | ./run_obelisk ./res/obelisk.cfg 42 | ``` 43 | 44 | Obelisk can use either public libbitcoin v4 servers, or your local 45 | libbitcoin-server if you have a running installation. Currently, 46 | **only testnet v4 public servers are available**, and they're set up 47 | as default in the configuration file. 48 | 49 | Obelisk can also be installed with setuptools: 50 | 51 | ``` 52 | python3 setup.py install --user 53 | ``` 54 | 55 | 56 | Development 57 | ----------- 58 | 59 | The code is written to be short and concise. `run_obelisk` is the 60 | entry point to start the server, but most of the actual logic is 61 | in `obelisk/protocol.py` and `obelisk/zeromq.py`. `protocol.py` 62 | implements the ElectrumX protocol, and `zeromq.py` implements the 63 | libbitcoin side of things. 64 | 65 | Before committing code, please run `make format` to format 66 | the codebase to a certain code style. This script depends on 67 | [yapf](https://github.com/google/yapf). 68 | 69 | It is also recommended to run the test suite and see if anything 70 | fails: 71 | 72 | ``` 73 | make test 74 | ``` 75 | 76 | You can chat about Obelisk on Freenode IRC, either `#electrum` or 77 | `#libbitcoin`. 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 0 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /obelisk/merkle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2020-2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """Module for calculating merkle branches""" 18 | from math import ceil, log 19 | 20 | from obelisk.util import double_sha256, hash_to_hex_str 21 | 22 | 23 | def branch_length(hash_count): # pragma: no cover 24 | """Return the length of a merkle branch given the number of hashes""" 25 | if not isinstance(hash_count, int): 26 | raise TypeError("hash_count must be an integer") 27 | if hash_count < 1: 28 | raise ValueError("hash_count must be at least 1") 29 | return ceil(log(hash_count, 2)) 30 | 31 | 32 | def merkle_branch_and_root(hashes, index, length=None): 33 | """Return a (merkle branch, merkle_root) pair given hashes, and the 34 | index of one of those hashes. 35 | """ 36 | hashes = list(hashes) 37 | if not isinstance(index, int): 38 | raise TypeError("index must be an integer") # pragma: no cover 39 | # This also asserts hashes is not empty 40 | if not 0 <= index < len(hashes): 41 | raise ValueError("index out of range") # pragma: no cover 42 | natural_length = branch_length(len(hashes)) 43 | if length is None: 44 | length = natural_length 45 | else: # pragma: no cover 46 | if not isinstance(length, int): 47 | raise TypeError("length must be an integer") 48 | if length < natural_length: 49 | raise ValueError("length out of range") 50 | 51 | branch = [] 52 | for _ in range(length): 53 | if len(hashes) & 1: 54 | hashes.append(hashes[-1]) 55 | branch.append(hashes[index ^ 1]) 56 | index >>= 1 57 | hashes = [ 58 | double_sha256(hashes[n] + hashes[n + 1]) 59 | for n in range(0, len(hashes), 2) 60 | ] 61 | return branch, hashes[0] 62 | 63 | 64 | def merkle_branch(tx_hashes, tx_pos): 65 | """Return a merkle branch given hashes and the tx position""" 66 | branch, _ = merkle_branch_and_root(tx_hashes, tx_pos) 67 | return [hash_to_hex_str(h) for h in branch] 68 | -------------------------------------------------------------------------------- /obelisk/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2020-2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """Utility functions""" 18 | import hashlib 19 | from binascii import hexlify 20 | 21 | _sha256 = hashlib.sha256 22 | 23 | 24 | def is_integer(val): 25 | """Check if val is of type int""" 26 | return isinstance(val, int) 27 | 28 | 29 | def is_non_negative_integer(val): 30 | """Check if val is of type int and non-negative""" 31 | if is_integer(val): 32 | return val >= 0 33 | return False 34 | 35 | 36 | def is_boolean(val): 37 | """Check if val is of type bool""" 38 | return isinstance(val, bool) 39 | 40 | 41 | def is_hex_str(text): 42 | """Check if text is a hex string""" 43 | if not isinstance(text, str): 44 | return False 45 | try: 46 | b = bytes.fromhex(text) 47 | except: # pylint: disable=W0702 48 | return False 49 | # Forbid whitespaces in text: 50 | if len(text) != 2 * len(b): 51 | return False 52 | return True 53 | 54 | 55 | def is_hash256_str(text): 56 | """Check if text is a sha256 hash""" 57 | if not isinstance(text, str): 58 | return False 59 | if len(text) != 64: 60 | return False 61 | return is_hex_str(text) 62 | 63 | 64 | def safe_hexlify(val): 65 | """hexlify and return a string""" 66 | return str(hexlify(val), "utf-8") 67 | 68 | 69 | def bh2u(val): 70 | """ 71 | str with hex representation of a bytes-like object 72 | 73 | >>> x = bytes((1, 2, 10)) 74 | >>> bh2u(x) 75 | '01020A' 76 | """ 77 | return val.hex() 78 | 79 | 80 | def block_to_header(block): # pragma: no cover 81 | """Return block header from raw block""" 82 | if not isinstance(block, (bytes, bytearray)): 83 | raise ValueError("block is not of type bytes/bytearray") 84 | block_header = block[:80] 85 | # version = block_header[:4] 86 | # prev_merkle_root = block_header[4:36] 87 | # merkle_root = block_header[36:68] 88 | # timestamp = block_header[68:72] 89 | # bits = block_header[72:76] 90 | # nonce = block_header[76:80] 91 | return block_header 92 | 93 | 94 | def sha256(inp): 95 | """ Simple wrapper of hashlib sha256. """ 96 | return _sha256(inp).digest() 97 | 98 | 99 | def double_sha256(inp): 100 | """ sha256 of sha256, as used extensively in bitcoin """ 101 | return sha256(sha256(inp)) 102 | 103 | 104 | def hash_to_hex_str(inp): 105 | """Convert a big-endian binary hash to displayed hex string. 106 | Display form of a binary hash is reversed and converted to hex. 107 | """ 108 | return bytes(reversed(inp)).hex() 109 | -------------------------------------------------------------------------------- /obelisk/obelisk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2020-2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | import asyncio 18 | import sys 19 | from argparse import ArgumentParser 20 | from configparser import RawConfigParser, NoSectionError 21 | from logging import getLogger, FileHandler, Formatter, StreamHandler 22 | from os import devnull 23 | 24 | from obelisk.protocol import ElectrumProtocol, VERSION 25 | 26 | # Used for destructor/cleanup 27 | PROTOCOL = None 28 | 29 | 30 | def logger_config(log, config): 31 | """Setup logging""" 32 | fmt = Formatter("%(asctime)s\t%(levelname)s\t%(message)s") 33 | logstream = StreamHandler() 34 | logstream.setFormatter(fmt) 35 | debuglevel = config.get("obelisk", "log_level", fallback="INFO") 36 | logstream.setLevel(debuglevel) 37 | log.addHandler(logstream) 38 | filename = config.get("obelisk", "log_file", fallback=devnull) 39 | append_log = config.getboolean("obelisk", "append_log", fallback=False) 40 | logfile = FileHandler(filename, mode=("a" if append_log else "w")) 41 | logfile.setFormatter(fmt) 42 | logfile.setLevel(debuglevel) 43 | log.addHandler(logfile) 44 | log.setLevel(debuglevel) 45 | return log, filename 46 | 47 | 48 | async def run_electrum_server(config, chain): 49 | """Server coroutine""" 50 | log = getLogger("obelisk") 51 | host = config.get("obelisk", "host") 52 | port = int(config.get("obelisk", "port")) 53 | 54 | endpoints = {} 55 | endpoints["query"] = config.get("obelisk", "query") 56 | endpoints["heart"] = config.get("obelisk", "heart") 57 | endpoints["block"] = config.get("obelisk", "block") 58 | endpoints["trans"] = config.get("obelisk", "trans") 59 | 60 | server_cfg = {} 61 | hostname_list = config.get("obelisk", "hostname").split(",") 62 | server_cfg["server_hostnames"] = hostname_list 63 | server_cfg["server_port"] = port 64 | 65 | global PROTOCOL 66 | PROTOCOL = ElectrumProtocol(log, chain, endpoints, server_cfg) 67 | 68 | server = await asyncio.start_server(PROTOCOL.recv, host, port) 69 | async with server: 70 | await server.serve_forever() 71 | 72 | 73 | def main(): 74 | """Main orchestration""" 75 | parser = ArgumentParser(description=f"obelisk {VERSION}") 76 | parser.add_argument("config_file", help="Path to config file") 77 | args = parser.parse_args() 78 | 79 | try: 80 | config = RawConfigParser() 81 | config.read(args.config_file) 82 | config.options("obelisk") 83 | except NoSectionError: 84 | print(f"error: Invalid config file {args.config_file}") 85 | return 1 86 | 87 | log = getLogger("obelisk") 88 | log, logfilename = logger_config(log, config) 89 | log.info(f"Starting obelisk {VERSION}") 90 | log.info(f"Logging to {logfilename}") 91 | 92 | chain = config.get("obelisk", "chain") 93 | if chain not in ("mainnet", "testnet"): 94 | log.error("chain is not 'mainnet' or 'testnet'") 95 | return 1 96 | 97 | try: 98 | asyncio.run(run_electrum_server(config, chain)) 99 | except KeyboardInterrupt: 100 | print("\r", end="") 101 | log.debug("Caught KeyboardInterrupt, exiting...") 102 | if PROTOCOL: 103 | asyncio.run(PROTOCOL.stop()) 104 | return 0 105 | 106 | return 1 107 | 108 | 109 | if __name__ == "__main__": 110 | sys.exit(main()) 111 | -------------------------------------------------------------------------------- /obelisk/errors_libbitcoin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """Enumerated error codes that can be returned by libbitcoin""" 18 | from enum import Enum 19 | 20 | 21 | def make_error_code(ec): 22 | """Return ErrorCode from ec""" 23 | if not ec: 24 | return None 25 | return ZMQError(ec) # pragma: no cover 26 | 27 | 28 | class ZMQError(Enum): 29 | """libbitcoin error codes""" 30 | 31 | # general codes 32 | success = 0 33 | deprecated1 = 6 34 | unknown = 43 35 | not_found = 3 36 | file_system = 42 37 | not_implemented = 4 38 | oversubscribed = 71 39 | 40 | # network 41 | service_stopped = 1 42 | operation_failed = 2 43 | resolve_failed = 7 44 | network_unreachable = 8 45 | address_in_use = 9 46 | listen_failed = 10 47 | accept_failed = 11 48 | bad_stream = 12 49 | channel_timeout = 13 50 | address_blocked = 44 51 | channel_stopped = 45 52 | peer_throttling = 73 53 | 54 | # database 55 | store_block_duplicate = 66 56 | store_block_invalid_height = 67 57 | store_block_missing_parent = 68 58 | store_lock_failure = 85 59 | store_incorrect_state = 86 60 | 61 | # blockchain 62 | duplicate_block = 51 63 | orphan_block = 5 64 | invalid_previous_block = 24 65 | insufficient_work = 48 66 | duplicate_transaction = 84 67 | orphan_transaction = 14 68 | transaction_version = 17 69 | insufficient_fee = 70 70 | stale_chain = 75 71 | dusty_transaction = 76 72 | 73 | # check header 74 | invalid_proof_of_work = 26 75 | futuristic_timestamp = 27 76 | 77 | # accept header 78 | checkpoints_failed = 35 79 | invalid_block_version = 36 80 | incorrect_proof_of_work = 32 81 | timestamp_too_early = 33 82 | 83 | # check block 84 | block_size_limit = 50 85 | empty_block = 47 86 | first_not_coinbase = 28 87 | extra_coinbases = 29 88 | internal_duplicate = 49 89 | block_internal_double_spend = 15 90 | forward_reference = 79 91 | merkle_mismatch = 31 92 | block_legacy_sigop_limit = 30 93 | 94 | # accept block 95 | block_non_final = 34 96 | coinbase_height_mismatch = 37 97 | coinbase_value_limit = 41 98 | block_embedded_sigop_limit = 52 99 | invalid_witness_commitment = 25 100 | block_weight_limit = 82 101 | 102 | # check transaction 103 | empty_transaction = 20 104 | previous_output_null = 23 105 | spend_overflow = 21 106 | invalid_coinbase_script_size = 22 107 | coinbase_transaction = 16 108 | transaction_internal_double_spend = 72 109 | transaction_size_limit = 53 110 | transaction_legacy_sigop_limit = 54 111 | 112 | # accept transaction 113 | transaction_non_final = 74 114 | premature_validation = 69 115 | unspent_duplicate = 38 116 | missing_previous_output = 19 117 | double_spend = 18 118 | coinbase_maturity = 46 119 | spend_exceeds_value = 40 120 | transaction_embedded_sigop_limit = 55 121 | sequence_locked = 78 122 | transaction_weight_limit = 83 123 | 124 | # connect input 125 | invalid_script = 39 126 | invalid_script_size = 56 127 | invalid_push_data_size = 57 128 | invalid_operation_count = 58 129 | invalid_stack_size = 59 130 | invalid_stack_scope = 60 131 | invalid_script_embed = 61 132 | invalid_signature_encoding = 62 133 | deprecated2 = 63 134 | incorrect_signature = 64 135 | unexpected_witness = 77 136 | invalid_witness = 80 137 | dirty_witness = 81 138 | stack_false = 65 139 | 140 | # http 141 | http_invalid_request = 90 142 | http_method_not_found = 91 143 | http_internal_error = 92 144 | -------------------------------------------------------------------------------- /tests/test_electrum_protocol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """ 18 | Test unit for the Electrum protocol. Takes results from testnet 19 | blockstream.info:143 server as value reference. 20 | 21 | See bottom of file for test orchestration. 22 | """ 23 | import asyncio 24 | import json 25 | import sys 26 | import traceback 27 | from logging import getLogger 28 | from pprint import pprint 29 | from socket import socket, AF_INET, SOCK_STREAM 30 | 31 | from obelisk.errors_jsonrpc import JsonRPCError 32 | from obelisk.protocol import ( 33 | ElectrumProtocol, 34 | VERSION, 35 | SERVER_PROTO_MIN, 36 | SERVER_PROTO_MAX, 37 | ) 38 | from obelisk.zeromq import create_random_id 39 | 40 | libbitcoin = { 41 | "query": "tcp://testnet2.libbitcoin.net:29091", 42 | "heart": "tcp://testnet2.libbitcoin.net:29092", 43 | "block": "tcp://testnet2.libbitcoin.net:29093", 44 | "trans": "tcp://testnet2.libbitcoin.net:29094", 45 | } 46 | 47 | blockstream = ("blockstream.info", 143) 48 | bs = None # Socket 49 | 50 | 51 | def get_expect(method, params): 52 | global bs 53 | req = { 54 | "json-rpc": "2.0", 55 | "id": create_random_id(), 56 | "method": method, 57 | "params": params 58 | } 59 | bs.send(json.dumps(req).encode("utf-8") + b"\n") 60 | recv_buf = bytearray() 61 | while True: 62 | data = bs.recv(4096) 63 | if not data or len(data) == 0: # pragma: no cover 64 | raise ValueError("No data received from blockstream") 65 | recv_buf.extend(data) 66 | lb = recv_buf.find(b"\n") 67 | if lb == -1: # pragma: no cover 68 | continue 69 | while lb != -1: 70 | line = recv_buf[:lb].rstrip() 71 | recv_buf = recv_buf[lb + 1:] 72 | lb = recv_buf.find(b"\n") 73 | line = line.decode("utf-8") 74 | resp = json.loads(line) 75 | return resp 76 | 77 | 78 | def assert_equal(data, expect): # pragma: no cover 79 | try: 80 | assert data == expect 81 | except AssertionError: 82 | print("Got:") 83 | pprint(data) 84 | print("Expected:") 85 | pprint(expect) 86 | raise 87 | 88 | 89 | async def test_server_version(protocol, writer, method): 90 | params = ["obelisk 42", [SERVER_PROTO_MIN, SERVER_PROTO_MAX]] 91 | expect = {"result": [f"obelisk {VERSION}", SERVER_PROTO_MAX]} 92 | data = await protocol.server_version(writer, {"params": params}) 93 | assert_equal(data["result"], expect["result"]) 94 | 95 | params = ["obelisk", "0.0"] 96 | expect = JsonRPCError.protonotsupported() 97 | data = await protocol.server_version(writer, {"params": params}) 98 | assert_equal(data, expect) 99 | 100 | params = ["obelisk"] 101 | expect = JsonRPCError.invalidparams() 102 | data = await protocol.server_version(writer, {"params": params}) 103 | assert_equal(data, expect) 104 | 105 | 106 | async def test_ping(protocol, writer, method): 107 | params = [] 108 | expect = get_expect(method, params) 109 | data = await protocol.ping(writer, {"params": params}) 110 | assert_equal(data["result"], expect["result"]) 111 | 112 | 113 | async def test_block_header(protocol, writer, method): 114 | params = [[123], [1, 5]] 115 | for i in params: 116 | expect = get_expect(method, i) 117 | data = await protocol.block_header(writer, {"params": i}) 118 | assert_equal(data["result"], expect["result"]) 119 | 120 | params = [[], [-3], [4, -1], [5, 3]] 121 | for i in params: 122 | expect = JsonRPCError.invalidparams() 123 | data = await protocol.block_header(writer, {"params": i}) 124 | assert_equal(data, expect) 125 | 126 | 127 | async def test_block_headers(protocol, writer, method): 128 | params = [[123, 3], [11, 3, 14]] 129 | for i in params: 130 | expect = get_expect(method, i) 131 | data = await protocol.block_headers(writer, {"params": i}) 132 | assert_equal(data["result"], expect["result"]) 133 | 134 | params = [[], [1], [-3, 1], [4, -1], [7, 4, 4]] 135 | for i in params: 136 | expect = JsonRPCError.invalidparams() 137 | data = await protocol.block_headers(writer, {"params": i}) 138 | assert_equal(data, expect) 139 | 140 | 141 | async def test_estimatefee(protocol, writer, method): 142 | params = [2] 143 | expect = 0.00001 144 | data = await protocol.estimatefee(writer, {"params": params}) 145 | assert_equal(data["result"], expect) 146 | 147 | 148 | async def test_headers_subscribe(protocol, writer, method): 149 | params = [[]] 150 | for i in params: 151 | expect = get_expect(method, i) 152 | data = await protocol.headers_subscribe(writer, {"params": i}) 153 | assert_equal(data["result"], expect["result"]) 154 | 155 | 156 | async def test_relayfee(protocol, writer, method): 157 | expect = 0.00001 158 | data = await protocol.relayfee(writer, {"params": []}) 159 | assert_equal(data["result"], expect) 160 | 161 | 162 | async def test_scripthash_get_balance(protocol, writer, method): 163 | params = [ 164 | ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"], 165 | ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"], 166 | ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"], 167 | ] 168 | for i in params: 169 | expect = get_expect(method, i) 170 | data = await protocol.scripthash_get_balance(writer, {"params": i}) 171 | assert_equal(data["result"], expect["result"]) 172 | 173 | params = [ 174 | [], 175 | ["foobar"], 176 | [ 177 | "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", 178 | 42, 179 | ], 180 | ] 181 | for i in params: 182 | expect = JsonRPCError.invalidparams() 183 | data = await protocol.scripthash_get_balance(writer, {"params": i}) 184 | assert_equal(data, expect) 185 | 186 | 187 | async def test_scripthash_get_history(protocol, writer, method): 188 | params = [ 189 | ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"], 190 | ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"], 191 | ] 192 | for i in params: 193 | expect = get_expect(method, i) 194 | data = await protocol.scripthash_get_history(writer, {"params": i}) 195 | assert_equal(data["result"], expect["result"]) 196 | 197 | params = [ 198 | [], 199 | ["foobar"], 200 | [ 201 | "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", 202 | 42, 203 | ], 204 | ] 205 | for i in params: 206 | expect = JsonRPCError.invalidparams() 207 | data = await protocol.scripthash_get_history(writer, {"params": i}) 208 | assert_equal(data, expect) 209 | 210 | 211 | async def test_scripthash_listunspent(protocol, writer, method): 212 | params = [ 213 | ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"], 214 | ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"], 215 | ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"], 216 | ] 217 | for i in params: 218 | # Blockstream is broken here and doesn't return in ascending order. 219 | expect = get_expect(method, i) 220 | srt = sorted(expect["result"], key=lambda x: x["height"]) 221 | data = await protocol.scripthash_listunspent(writer, {"params": i}) 222 | assert_equal(data["result"], srt) 223 | 224 | params = [ 225 | [], 226 | ["foobar"], 227 | [ 228 | "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", 229 | 42, 230 | ], 231 | ] 232 | for i in params: 233 | expect = JsonRPCError.invalidparams() 234 | data = await protocol.scripthash_listunspent(writer, {"params": i}) 235 | assert_equal(data, expect) 236 | 237 | 238 | async def test_scripthash_subscribe(protocol, writer, method): 239 | params = [ 240 | ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"], 241 | ] 242 | for i in params: 243 | expect = get_expect(method, i) 244 | data = await protocol.scripthash_subscribe(writer, {"params": i}) 245 | assert_equal(data["result"], expect["result"]) 246 | 247 | params = [ 248 | [], 249 | ["foobar"], 250 | [ 251 | "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", 252 | 42, 253 | ], 254 | ] 255 | for i in params: 256 | expect = JsonRPCError.invalidparams() 257 | data = await protocol.scripthash_subscribe(writer, {"params": i}) 258 | assert_equal(data, expect) 259 | 260 | 261 | async def test_scripthash_unsubscribe(protocol, writer, method): 262 | # Here blockstream doesn't even care 263 | params = [ 264 | ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"], 265 | ] 266 | for i in params: 267 | data = await protocol.scripthash_unsubscribe(writer, {"params": i}) 268 | assert data["result"] is True 269 | 270 | params = [ 271 | [], 272 | ["foobar"], 273 | [ 274 | "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", 275 | 42, 276 | ], 277 | ] 278 | for i in params: 279 | expect = JsonRPCError.invalidparams() 280 | data = await protocol.scripthash_unsubscribe(writer, {"params": i}) 281 | assert_equal(data, expect) 282 | 283 | 284 | async def test_transaction_get(protocol, writer, method): 285 | params = [ 286 | ["a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20"], 287 | ] 288 | for i in params: 289 | expect = get_expect(method, i) 290 | data = await protocol.transaction_get(writer, {"params": i}) 291 | assert_equal(data["result"], expect["result"]) 292 | 293 | params = [[], [1], ["foo"], ["dead beef"]] 294 | for i in params: 295 | expect = JsonRPCError.invalidparams() 296 | data = await protocol.transaction_get(writer, {"params": i}) 297 | assert_equal(data, expect) 298 | 299 | 300 | async def test_transaction_get_merkle(protocol, writer, method): 301 | params = [ 302 | [ 303 | "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20", 304 | 1970700, 305 | ], 306 | ] 307 | for i in params: 308 | expect = get_expect(method, i) 309 | data = await protocol.transaction_get_merkle(writer, {"params": i}) 310 | assert_equal(data["result"], expect["result"]) 311 | 312 | params = [ 313 | [], 314 | ["foo", 1], 315 | [3, 1], 316 | [ 317 | "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20", 318 | -4, 319 | ], 320 | [ 321 | "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20", 322 | "foo", 323 | ], 324 | ] 325 | for i in params: 326 | expect = JsonRPCError.invalidparams() 327 | data = await protocol.transaction_get_merkle(writer, {"params": i}) 328 | assert_equal(data, expect) 329 | 330 | 331 | async def test_transaction_id_from_pos(protocol, writer, method): 332 | params = [[1970700, 28], [1970700, 28, True]] 333 | for i in params: 334 | expect = get_expect(method, i) 335 | data = await protocol.transaction_id_from_pos(writer, {"params": i}) 336 | assert_equal(data["result"], expect["result"]) 337 | 338 | params = [[123], [-1, 1], [1, -1], [3, 42, 4]] 339 | for i in params: 340 | expect = JsonRPCError.invalidparams() 341 | data = await protocol.transaction_id_from_pos(writer, {"params": i}) 342 | assert_equal(data, expect) 343 | 344 | 345 | async def test_get_fee_histogram(protocol, writer, method): 346 | data = await protocol.get_fee_histogram(writer, {"params": []}) 347 | assert_equal(data["result"], [[0, 0]]) 348 | 349 | 350 | async def test_add_peer(protocol, writer, method): 351 | data = await protocol.add_peer(writer, {"params": []}) 352 | assert_equal(data["result"], False) 353 | 354 | 355 | async def test_banner(protocol, writer, method): 356 | data = await protocol.banner(writer, {"params": []}) 357 | assert_equal(type(data["result"]), str) 358 | 359 | 360 | async def test_donation_address(protocol, writer, method): 361 | data = await protocol.donation_address(writer, {"params": []}) 362 | assert_equal(type(data["result"]), str) 363 | 364 | 365 | async def test_peers_subscribe(protocol, writer, method): 366 | data = await protocol.peers_subscribe(writer, {"params": []}) 367 | assert_equal(data["result"], []) 368 | 369 | 370 | async def test_send_notification(protocol, writer, method): 371 | params = ["sent notification"] 372 | expect = (json.dumps({ 373 | "jsonrpc": "2.0", 374 | "method": method, 375 | "params": params 376 | }).encode("utf-8") + b"\n") 377 | await protocol._send_notification(writer, method, params) 378 | assert_equal(writer.mock, expect) 379 | 380 | 381 | async def test_send_reply(protocol, writer, method): 382 | error = {"error": {"code": 42, "message": 42}} 383 | result = {"result": 42} 384 | 385 | expect = (json.dumps({ 386 | "jsonrpc": "2.0", 387 | "error": error["error"], 388 | "id": None 389 | }).encode("utf-8") + b"\n") 390 | await protocol._send_reply(writer, error, None) 391 | assert_equal(writer.mock, expect) 392 | 393 | expect = (json.dumps({ 394 | "jsonrpc": "2.0", 395 | "result": result["result"], 396 | "id": 42 397 | }).encode("utf-8") + b"\n") 398 | await protocol._send_reply(writer, result, {"id": 42}) 399 | assert_equal(writer.mock, expect) 400 | 401 | 402 | async def test_handle_query(protocol, writer, method): 403 | query = {"jsonrpc": "2.0", "method": method, "id": 42, "params": []} 404 | await protocol.handle_query(writer, query) 405 | 406 | method = "server.donation_address" 407 | query = {"jsonrpc": "2.0", "method": method, "id": 42, "params": []} 408 | await protocol.handle_query(writer, query) 409 | 410 | query = {"jsonrpc": "2.0", "method": method, "params": []} 411 | await protocol.handle_query(writer, query) 412 | 413 | query = {"jsonrpc": "2.0", "id": 42, "params": []} 414 | await protocol.handle_query(writer, query) 415 | 416 | 417 | class MockTransport: 418 | 419 | def __init__(self): 420 | self.peername = ("foo", 42) 421 | 422 | def get_extra_info(self, param): 423 | return self.peername 424 | 425 | 426 | class MockWriter(asyncio.StreamWriter): # pragma: no cover 427 | """Mock class for StreamWriter""" 428 | 429 | def __init__(self): 430 | self.mock = None 431 | self._transport = MockTransport() 432 | 433 | def write(self, data): 434 | self.mock = data 435 | return True 436 | 437 | async def drain(self): 438 | return True 439 | 440 | 441 | # Test orchestration 442 | orchestration = { 443 | "server.version": test_server_version, 444 | "server.ping": test_ping, 445 | "blockchain.block.header": test_block_header, 446 | "blockchain.block.headers": test_block_headers, 447 | "blockchain.estimatefee": test_estimatefee, 448 | "blockchain.headers.subscribe": test_headers_subscribe, 449 | "blockchain.relayfee": test_relayfee, 450 | "blockchain.scripthash.get_balance": test_scripthash_get_balance, 451 | "blockchain.scripthash.get_history": test_scripthash_get_history, 452 | # "blockchain.scripthash.get_mempool": test_scripthash_get_mempool, 453 | "blockchain.scripthash.listunspent": test_scripthash_listunspent, 454 | "blockchain.scripthash.subscribe": test_scripthash_subscribe, 455 | "blockchain.scripthash.unsubscribe": test_scripthash_unsubscribe, 456 | # "blockchain.transaction.broadcast": test_transaction_broadcast, 457 | "blockchain.transaction.get": test_transaction_get, 458 | "blockchain.transaction.get_merkle": test_transaction_get_merkle, 459 | "blockchain.transaction.id_from_pos": test_transaction_id_from_pos, 460 | "mempool.get_fee_histogram": test_get_fee_histogram, 461 | "server.add_peer": test_add_peer, 462 | "server.banner": test_banner, 463 | "server.donation_address": test_donation_address, 464 | # "server.features": test_server_features, 465 | "server.peers_subscribe": test_peers_subscribe, 466 | "_send_notification": test_send_notification, 467 | "_send_reply": test_send_reply, 468 | "_handle_query": test_handle_query, 469 | } 470 | 471 | 472 | async def main(): 473 | test_pass = [] 474 | test_fail = [] 475 | 476 | global bs 477 | bs = socket(AF_INET, SOCK_STREAM) 478 | bs.connect(blockstream) 479 | 480 | log = getLogger("obelisktest") 481 | protocol = ElectrumProtocol(log, "testnet", libbitcoin, {}) 482 | writer = MockWriter() 483 | 484 | protocol.peers[protocol._get_peer(writer)] = {"tasks": [], "sh": {}} 485 | 486 | for func in orchestration: 487 | try: 488 | await orchestration[func](protocol, writer, func) 489 | print(f"PASS: {func}") 490 | test_pass.append(func) 491 | except AssertionError: # pragma: no cover 492 | print(f"FAIL: {func}") 493 | traceback.print_exc() 494 | test_fail.append(func) 495 | 496 | bs.close() 497 | await protocol.stop() 498 | 499 | print() 500 | print(f"Tests passed: {len(test_pass)}") 501 | print(f"Tests failed: {len(test_fail)}") 502 | 503 | ret = 1 if len(test_fail) > 0 else 0 504 | sys.exit(ret) 505 | -------------------------------------------------------------------------------- /obelisk/zeromq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2020-2021 Ivan J. 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """ZeroMQ implementation for libbitcoin""" 18 | import asyncio 19 | import functools 20 | import struct 21 | from binascii import unhexlify 22 | from random import randint 23 | 24 | import zmq 25 | import zmq.asyncio 26 | 27 | from obelisk.errors_libbitcoin import make_error_code, ZMQError 28 | from obelisk.util import hash_to_hex_str 29 | 30 | 31 | def create_random_id(): 32 | """Generate a random request ID""" 33 | max_uint32 = 4294967295 34 | return randint(0, max_uint32) 35 | 36 | 37 | def pack_block_index(index): 38 | """struct.pack given index""" 39 | if isinstance(index, str): 40 | index = unhexlify(index) 41 | assert len(index) == 32 42 | return index 43 | if isinstance(index, int): 44 | return struct.pack(" 0 184 | 185 | def __str__(self): 186 | return ( 187 | "Response(command, request ID, error code, data):" + 188 | f" {self.command}, {self.request_id}, {self.error_code}, {self.data}" 189 | ) 190 | 191 | 192 | class RequestCollection: 193 | """RequestCollection carries a list of Requests and matches incoming 194 | responses to them. 195 | """ 196 | 197 | def __init__(self, socket, loop): 198 | self._socket = socket 199 | self._requests = {} 200 | self._task = asyncio.ensure_future(self._run(), loop=loop) 201 | 202 | async def _run(self): 203 | while True: 204 | await self._receive() 205 | 206 | async def stop(self): 207 | """Stops listening for incoming responses (or subscription) messages. 208 | Returns the number of _responses_ expected but which are now dropped 209 | on the floor. 210 | """ 211 | self._task.cancel() 212 | try: 213 | await self._task 214 | except asyncio.CancelledError: 215 | return len(self._requests) 216 | 217 | async def _receive(self): 218 | frame = await self._socket.recv_multipart() 219 | response = Response(frame) 220 | 221 | if response.request_id in self._requests: 222 | self._handle_response(response) 223 | else: 224 | print("DEBUG; RequestCollection unhandled response %s:%s" % 225 | (response.command, response.request_id)) 226 | 227 | def _handle_response(self, response): 228 | request = self._requests[response.request_id] 229 | 230 | if request.is_subscription(): 231 | if response.is_bound_for_queue(): 232 | # TODO: decode the data into something usable 233 | request.queue.put_nowait(response.data) 234 | else: 235 | request.future.set_result(response) 236 | else: 237 | self.delete_request(request) 238 | request.future.set_result(response) 239 | 240 | def add_request(self, request): 241 | # TODO: we should maybe check if the request.id_ is unique 242 | self._requests[request.id_] = request 243 | 244 | def delete_request(self, request): 245 | del self._requests[request.id_] 246 | 247 | 248 | class Client: 249 | """This class represents a connection to a libbitcoin server.""" 250 | 251 | def __init__(self, log, endpoints, loop): 252 | self.log = log 253 | self._endpoints = endpoints 254 | self._settings = ClientSettings(loop=loop) 255 | self._query_socket = self._create_query_socket() 256 | self._block_socket = self._create_block_socket() 257 | self._request_collection = RequestCollection(self._query_socket, 258 | self._settings._loop) 259 | 260 | async def stop(self): 261 | self.log.debug("zmq Client.stop()") 262 | self._query_socket.close() 263 | self._block_socket.close() 264 | return await self._request_collection.stop() 265 | 266 | def _create_block_socket(self): 267 | socket = self._settings.context.socket( 268 | zmq.SUB, # pylint: disable=E1101 269 | io_loop=self._settings._loop, # pylint: disable=W0212 270 | ) 271 | socket.connect(self._endpoints["block"]) 272 | socket.setsockopt_string(zmq.SUBSCRIBE, "") # pylint: disable=E1101 273 | return socket 274 | 275 | def _create_query_socket(self): 276 | socket = self._settings.context.socket( 277 | zmq.DEALER, # pylint: disable=E1101 278 | io_loop=self._settings._loop, # pylint: disable=W0212 279 | ) 280 | socket.connect(self._endpoints["query"]) 281 | return socket 282 | 283 | async def _subscription_request(self, command, data, queue): 284 | request = await self._request(command, data) 285 | request.queue = queue 286 | error_code, _ = await self._wait_for_response(request) 287 | return error_code 288 | 289 | async def _simple_request(self, command, data): 290 | return await self._wait_for_response(await self._request(command, data)) 291 | 292 | async def _request(self, command, data): 293 | """Make a generic request. Both options are byte objects specified 294 | like b'blockchain.fetch_block_header' as an example. 295 | """ 296 | request = Request(self._query_socket, command, data) 297 | await request.send() 298 | self._request_collection.add_request(request) 299 | return request 300 | 301 | async def _wait_for_response(self, request): 302 | try: 303 | response = await asyncio.wait_for(request.future, 304 | self._settings.timeout) 305 | except asyncio.TimeoutError: 306 | self._request_collection.delete_request(request) 307 | return ZMQError.channel_timeout, None 308 | 309 | assert response.command == request.command 310 | assert response.request_id == request.id_ 311 | return response.error_code, response.data 312 | 313 | async def server_version(self): 314 | """Get the libbitcoin-server version""" 315 | command = b"server.version" 316 | error_code, data = await self._simple_request(command, b"") 317 | if error_code: 318 | return error_code, None 319 | return error_code, data 320 | 321 | async def fetch_last_height(self): 322 | """Fetch the blockchain tip and return integer height""" 323 | command = b"blockchain.fetch_last_height" 324 | error_code, data = await self._simple_request(command, b"") 325 | if error_code: 326 | return error_code, None 327 | return error_code, struct.unpack(" 3 | # 4 | # This file is part of obelisk 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License version 3 8 | # as published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | """Implementation of the Electrum protocol as found on 18 | https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html 19 | """ 20 | import asyncio 21 | import json 22 | import struct 23 | from binascii import unhexlify 24 | from traceback import print_exc 25 | 26 | from obelisk.errors_jsonrpc import JsonRPCError 27 | from obelisk.errors_libbitcoin import ZMQError 28 | from obelisk.mempool_api import get_mempool_fee_estimates, get_fee_histogram 29 | from obelisk.merkle import merkle_branch, merkle_branch_and_root 30 | from obelisk.util import ( 31 | bh2u, 32 | block_to_header, 33 | is_boolean, 34 | is_hash256_str, 35 | is_hex_str, 36 | is_non_negative_integer, 37 | safe_hexlify, 38 | sha256, 39 | double_sha256, 40 | hash_to_hex_str, 41 | ) 42 | from obelisk.zeromq import Client 43 | 44 | VERSION = "0.0" 45 | SERVER_PROTO_MIN = "1.4" 46 | SERVER_PROTO_MAX = "1.4.2" 47 | DONATION_ADDR = "bc1q7an9p5pz6pjwjk4r48zke2yfaevafzpglg26mz" 48 | 49 | BANNER = (""" 50 | Welcome to obelisk 51 | 52 | "Tools for the people" 53 | 54 | obelisk is a server that uses libbitcoin-server as its backend. 55 | Source code can be found at: https://github.com/parazyd/obelisk 56 | 57 | Please consider donating: %s 58 | """ % DONATION_ADDR) 59 | 60 | 61 | class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 62 | """Class implementing the Electrum protocol, with async support""" 63 | 64 | def __init__(self, log, chain, endpoints, server_cfg): 65 | self.log = log 66 | self.stopped = False 67 | self.endpoints = endpoints 68 | self.server_cfg = server_cfg 69 | self.loop = asyncio.get_event_loop() 70 | self.bx = Client(log, endpoints, self.loop) 71 | self.block_queue = None 72 | self.peers = {} 73 | 74 | self.chain = chain 75 | if self.chain == "mainnet": # pragma: no cover 76 | self.genesis = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 77 | elif self.chain == "testnet": 78 | self.genesis = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" 79 | else: 80 | raise ValueError(f"Invalid chain '{chain}'") # pragma: no cover 81 | 82 | # Here we map available methods to their respective functions 83 | self.methodmap = { 84 | "blockchain.block.header": self.block_header, 85 | "blockchain.block.headers": self.block_headers, 86 | "blockchain.estimatefee": self.estimatefee, 87 | "blockchain.headers.subscribe": self.headers_subscribe, 88 | "blockchain.relayfee": self.relayfee, 89 | "blockchain.scripthash.get_balance": self.scripthash_get_balance, 90 | "blockchain.scripthash.get_history": self.scripthash_get_history, 91 | "blockchain.scripthash.get_mempool": self.scripthash_get_mempool, 92 | "blockchain.scripthash.listunspent": self.scripthash_listunspent, 93 | "blockchain.scripthash.subscribe": self.scripthash_subscribe, 94 | "blockchain.scripthash.unsubscribe": self.scripthash_unsubscribe, 95 | "blockchain.transaction.broadcast": self.transaction_broadcast, 96 | "blockchain.transaction.get": self.transaction_get, 97 | "blockchain.transaction.get_merkle": self.transaction_get_merkle, 98 | "blockchain.transaction.id_from_pos": self.transaction_id_from_pos, 99 | "mempool.get_fee_histogram": self.get_fee_histogram, 100 | "server_add_peer": self.add_peer, 101 | "server.banner": self.banner, 102 | "server.donation_address": self.donation_address, 103 | "server.features": self.server_features, 104 | "server.peers.subscribe": self.peers_subscribe, 105 | "server.ping": self.ping, 106 | "server.version": self.server_version, 107 | } 108 | 109 | async def stop(self): 110 | """Destructor function""" 111 | self.log.debug("ElectrumProtocol.stop()") 112 | self.stopped = True 113 | if self.bx: 114 | for i in self.peers: 115 | await self._peer_cleanup(i) 116 | await self.bx.stop() 117 | 118 | async def _peer_cleanup(self, peer): 119 | """Cleanup tasks and data for peer""" 120 | self.log.debug("Cleaning up data for %s", peer) 121 | for i in self.peers[peer]["tasks"]: 122 | i.cancel() 123 | for i in self.peers[peer]["sh"]: 124 | self.peers[peer]["sh"][i]["task"].cancel() 125 | 126 | @staticmethod 127 | def _get_peer(writer): 128 | peer_t = writer._transport.get_extra_info("peername") # pylint: disable=W0212 129 | return f"{peer_t[0]}:{peer_t[1]}" 130 | 131 | async def recv(self, reader, writer): 132 | """Loop ran upon a connection which acts as a JSON-RPC handler""" 133 | recv_buf = bytearray() 134 | self.peers[self._get_peer(writer)] = {"tasks": [], "sh": {}} 135 | 136 | while not self.stopped: 137 | data = await reader.read(4096) 138 | if not data or len(data) == 0: 139 | await self._peer_cleanup(self._get_peer(writer)) 140 | return 141 | recv_buf.extend(data) 142 | lb = recv_buf.find(b"\n") 143 | if lb == -1: 144 | continue 145 | while lb != -1: 146 | line = recv_buf[:lb].rstrip() 147 | recv_buf = recv_buf[lb + 1:] 148 | lb = recv_buf.find(b"\n") 149 | try: 150 | line = line.decode("utf-8") 151 | query = json.loads(line) 152 | except (UnicodeDecodeError, json.JSONDecodeError) as err: 153 | self.log.debug("%s", print_exc) 154 | self.log.debug("Decode error: %s", repr(err)) 155 | break 156 | self.log.debug("=> %s", line) 157 | await self.handle_query(writer, query) 158 | 159 | async def _send_notification(self, writer, method, params): 160 | """Send JSON-RPC notification to given writer""" 161 | response = {"jsonrpc": "2.0", "method": method, "params": params} 162 | self.log.debug("<= %s", response) 163 | writer.write(json.dumps(response).encode("utf-8") + b"\n") 164 | await writer.drain() 165 | 166 | async def _send_response(self, writer, result, nid): 167 | """Send successful JSON-RPC response to given writer""" 168 | response = {"jsonrpc": "2.0", "result": result, "id": nid} 169 | self.log.debug("<= %s", response) 170 | writer.write(json.dumps(response).encode("utf-8") + b"\n") 171 | await writer.drain() 172 | 173 | async def _send_error(self, writer, error): 174 | """Send JSON-RPC error to given writer""" 175 | response = {"jsonrpc": "2.0", "error": error, "id": None} 176 | self.log.debug("<= %s", response) 177 | writer.write(json.dumps(response).encode("utf-8") + b"\n") 178 | await writer.drain() 179 | 180 | async def _send_reply(self, writer, resp, query): 181 | """Wrap function for sending replies""" 182 | if "error" in resp: 183 | return await self._send_error(writer, resp["error"]) 184 | return await self._send_response(writer, resp["result"], query["id"]) 185 | 186 | async def handle_query(self, writer, query): # pylint: disable=R0915,R0912,R0911 187 | """Electrum protocol method handler mapper""" 188 | if "method" not in query or "id" not in query: 189 | return await self._send_reply(writer, JsonRPCError.invalidrequest(), 190 | None) 191 | 192 | method = query["method"] 193 | func = self.methodmap.get(method) 194 | if not func: 195 | self.log.error("Unhandled method %s, query=%s", method, query) 196 | return await self._send_reply(writer, JsonRPCError.methodnotfound(), 197 | query) 198 | resp = await func(writer, query) 199 | return await self._send_reply(writer, resp, query) 200 | 201 | async def _merkle_proof_for_headers(self, height, idx): 202 | """Extremely inefficient merkle proof for headers""" 203 | # The following works, but is extremely inefficient. 204 | # The best solution would be to figure something out in 205 | # libbitcoin-server 206 | cp_headers = [] 207 | 208 | for i in range(0, height + 1): 209 | _ec, data = await self.bx.fetch_block_header(i) 210 | if _ec and _ec != ZMQError.success: 211 | self.log.error("bx.fetch_block_header: %s", _ec.name) 212 | return JsonRPCError.internalerror() 213 | cp_headers.append(data) 214 | 215 | branch, root = merkle_branch_and_root( 216 | [double_sha256(i) for i in cp_headers], idx) 217 | 218 | return { 219 | "branch": [hash_to_hex_str(i) for i in branch], 220 | "header": safe_hexlify(cp_headers[idx]), 221 | "root": hash_to_hex_str(root), 222 | } 223 | 224 | async def block_header(self, writer, query): # pylint: disable=W0613,R0911 225 | """Method: blockchain.block.header 226 | Return the block header at the given height. 227 | """ 228 | if "params" not in query or len(query["params"]) < 1: 229 | return JsonRPCError.invalidparams() 230 | index = query["params"][0] 231 | cp_height = query["params"][1] if len(query["params"]) == 2 else 0 232 | 233 | if not is_non_negative_integer(index): 234 | return JsonRPCError.invalidparams() 235 | if not is_non_negative_integer(cp_height): 236 | return JsonRPCError.invalidparams() 237 | if cp_height != 0 and not index <= cp_height: 238 | return JsonRPCError.invalidparams() 239 | 240 | if cp_height == 0: 241 | _ec, header = await self.bx.fetch_block_header(index) 242 | if _ec and _ec != ZMQError.success: 243 | self.log.error("bx.fetch_block_header: %s", _ec.name) 244 | return JsonRPCError.internalerror() 245 | return {"result": safe_hexlify(header)} 246 | 247 | res = await self._merkle_proof_for_headers(cp_height, index) 248 | return {"result": res} 249 | 250 | async def block_headers(self, writer, query): # pylint: disable=W0613,R0911 251 | """Method: blockchain.block.headers 252 | Return a concatenated chunk of block headers from the main chain. 253 | """ 254 | if "params" not in query or len(query["params"]) < 2: 255 | return JsonRPCError.invalidparams() 256 | # Electrum doesn't allow max_chunk_size to be less than 2016 257 | # gopher://bitreich.org/9/memecache/convenience-store.mkv 258 | max_chunk_size = 2016 259 | start_height = query["params"][0] 260 | count = query["params"][1] 261 | cp_height = query["params"][2] if len(query["params"]) == 3 else 0 262 | 263 | if not is_non_negative_integer(start_height): 264 | return JsonRPCError.invalidparams() 265 | if not is_non_negative_integer(count): 266 | return JsonRPCError.invalidparams() 267 | # BUG: spec says <= cp_height 268 | if cp_height != 0 and not start_height + (count - 1) < cp_height: 269 | return JsonRPCError.invalidparams() 270 | 271 | count = min(count, max_chunk_size) 272 | headers = bytearray() 273 | for i in range(count): 274 | _ec, data = await self.bx.fetch_block_header(start_height + i) 275 | if _ec and _ec != ZMQError.success: 276 | self.log.error("bx.fetch_block_header: %s", _ec.name) 277 | return JsonRPCError.internalerror() 278 | headers.extend(data) 279 | 280 | resp = { 281 | "hex": safe_hexlify(headers), 282 | "count": len(headers) // 80, 283 | "max": max_chunk_size, 284 | } 285 | 286 | if cp_height > 0: 287 | data = await self._merkle_proof_for_headers( 288 | cp_height, start_height + (len(headers) // 80) - 1) 289 | resp["branch"] = data["branch"] 290 | resp["root"] = data["root"] 291 | 292 | return {"result": resp} 293 | 294 | async def estimatefee(self, writer, query): # pylint: disable=W0613,disable=R0911 295 | """Method: blockchain.estimatefee 296 | Return the estimated transaction fee per kilobyte for a transaction 297 | to be confirmed within a certain number of blocks. 298 | """ 299 | # NOTE: This solution is using the mempool.space API. 300 | # Let's try to eventually solve it with some internal logic. 301 | if "params" not in query or len(query["params"]) != 1: 302 | return JsonRPCError.invalidparams() 303 | 304 | num_blocks = query["params"][0] 305 | if not is_non_negative_integer(num_blocks): 306 | return JsonRPCError.invalidparams() 307 | 308 | if self.chain == "testnet": 309 | return {"result": 0.00001} 310 | 311 | fee_dict = get_mempool_fee_estimates() 312 | if not fee_dict: 313 | return {"result": -1} 314 | 315 | # Good enough. 316 | if num_blocks < 3: 317 | return {"result": "{:.8f}".format(fee_dict["fastestFee"] / 100000)} 318 | 319 | if num_blocks < 6: 320 | return {"result": "{:.8f}".format(fee_dict["halfHourFee"] / 100000)} 321 | 322 | if num_blocks < 10: 323 | return {"result": "{:.8f}".format(fee_dict["hourFee"] / 100000)} 324 | 325 | return {"result": "{:.8f}".format(fee_dict["minimumFee"] / 100000)} 326 | 327 | async def header_notifier(self, writer): 328 | self.block_queue = asyncio.Queue() 329 | await self.bx.subscribe_to_blocks(self.block_queue) 330 | while True: 331 | item = await self.block_queue.get() 332 | if len(item) != 3: 333 | self.log.debug("error: item from block queue len != 3") 334 | continue 335 | 336 | header = block_to_header(item[2]) 337 | params = [{"height": item[1], "hex": safe_hexlify(header)}] 338 | await self._send_notification(writer, 339 | "blockchain.headers.subscribe", 340 | params) 341 | 342 | async def headers_subscribe(self, writer, query): # pylint: disable=W0613 343 | """Method: blockchain.headers.subscribe 344 | Subscribe to receive block headers when a new block is found. 345 | """ 346 | # Tip height and header are returned upon request 347 | _ec, height = await self.bx.fetch_last_height() 348 | if _ec and _ec != ZMQError.success: 349 | self.log.error("bx.fetch_last_height: %s", _ec.name) 350 | return JsonRPCError.internalerror() 351 | _ec, tip_header = await self.bx.fetch_block_header(height) 352 | if _ec and _ec != ZMQError.success: 353 | self.log.error("bx.fetch_block_header: %s", _ec.name) 354 | return JsonRPCError.internalerror() 355 | 356 | self.peers[self._get_peer(writer)]["tasks"].append( 357 | asyncio.create_task(self.header_notifier(writer))) 358 | ret = {"height": height, "hex": safe_hexlify(tip_header)} 359 | return {"result": ret} 360 | 361 | async def relayfee(self, writer, query): # pylint: disable=W0613 362 | """Method: blockchain.relayfee 363 | Return the minimum fee a low-priority transaction must pay in order 364 | to be accepted to the daemon’s memory pool. 365 | """ 366 | return {"result": 0.00001} 367 | 368 | async def scripthash_get_balance(self, writer, query): # pylint: disable=W0613 369 | """Method: blockchain.scripthash.get_balance 370 | Return the confirmed and unconfirmed balances of a script hash. 371 | """ 372 | if "params" not in query or len(query["params"]) != 1: 373 | return JsonRPCError.invalidparams() 374 | 375 | if not is_hash256_str(query["params"][0]): 376 | return JsonRPCError.invalidparams() 377 | 378 | _ec, data = await self.bx.fetch_balance(query["params"][0]) 379 | if _ec and _ec != ZMQError.success: 380 | self.log.error("bx.fetch_balance: %s", _ec.name) 381 | return JsonRPCError.internalerror() 382 | 383 | ret = {"confirmed": data[0], "unconfirmed": data[1]} 384 | return {"result": ret} 385 | 386 | async def scripthash_get_history(self, writer, query): # pylint: disable=W0613 387 | """Method: blockchain.scripthash.get_history 388 | Return the confirmed and unconfirmed history of a script hash. 389 | """ 390 | if "params" not in query or len(query["params"]) != 1: 391 | return JsonRPCError.invalidparams() 392 | 393 | if not is_hash256_str(query["params"][0]): 394 | return JsonRPCError.invalidparams() 395 | 396 | _ec, data = await self.bx.fetch_history4(query["params"][0]) 397 | if _ec and _ec != ZMQError.success: 398 | self.log.error("bx.fetch_history4: %s", _ec.name) 399 | return JsonRPCError.internalerror() 400 | 401 | self.log.debug("hist: %s", data) 402 | ret = [] 403 | # TODO: mempool 404 | for i in data: 405 | if "received" in i: 406 | ret.append({ 407 | "height": i["received"]["height"], 408 | "tx_hash": hash_to_hex_str(i["received"]["hash"]), 409 | }) 410 | if "spent" in i: 411 | ret.append({ 412 | "height": i["spent"]["height"], 413 | "tx_hash": hash_to_hex_str(i["spent"]["hash"]), 414 | }) 415 | 416 | return {"result": ret} 417 | 418 | async def scripthash_get_mempool(self, writer, query): # pylint: disable=W0613 419 | """Method: blockchain.scripthash.get_mempool 420 | Return the unconfirmed transactions of a script hash. 421 | """ 422 | # TODO: Implement 423 | return JsonRPCError.invalidrequest() 424 | 425 | async def scripthash_listunspent(self, writer, query): # pylint: disable=W0613 426 | """Method: blockchain.scripthash.listunspent 427 | Return an ordered list of UTXOs sent to a script hash. 428 | """ 429 | if "params" not in query or len(query["params"]) != 1: 430 | return JsonRPCError.invalidparams() 431 | 432 | scripthash = query["params"][0] 433 | if not is_hash256_str(scripthash): 434 | return JsonRPCError.invalidparams() 435 | 436 | _ec, utxo = await self.bx.fetch_utxo(scripthash) 437 | if _ec and _ec != ZMQError.success: 438 | self.log.error("bx.fetch_utxo: %s", _ec.name) 439 | return JsonRPCError.internalerror() 440 | 441 | ret = [] 442 | for i in utxo: 443 | rec = i["received"] 444 | ret.append({ 445 | "tx_pos": rec["index"], 446 | "value": i["value"], 447 | "tx_hash": hash_to_hex_str(rec["hash"]), 448 | "height": rec["height"] if rec["height"] != 4294967295 else 0, 449 | }) 450 | 451 | return {"result": ret} 452 | 453 | async def scripthash_renewer(self, scripthash, queue): 454 | while True: 455 | try: 456 | self.log.debug("scriphash renewer: %s", scripthash) 457 | _ec = await self.bx.subscribe_scripthash(scripthash, queue) 458 | if _ec and _ec != ZMQError.success: 459 | self.log.error("bx.subscribe_scripthash: %s", _ec.name) 460 | await asyncio.sleep(60) 461 | except asyncio.CancelledError: 462 | self.log.debug("subscription cancelled: %s", scripthash) 463 | break 464 | 465 | async def scripthash_notifier(self, writer, scripthash): 466 | # TODO: Mempool 467 | # TODO: This is still flaky and not always notified. Investigate. 468 | self.log.debug("notifier") 469 | method = "blockchain.scripthash.subscribe" 470 | queue = asyncio.Queue() 471 | renew_task = asyncio.create_task( 472 | self.scripthash_renewer(scripthash, queue)) 473 | 474 | while True: 475 | try: 476 | item = await queue.get() 477 | _ec, height, txid = struct.unpack(" 1 else False 605 | 606 | if not is_hex_str(tx_hash): 607 | return JsonRPCError.invalidparams() 608 | 609 | # _ec, rawtx = await self.bx.fetch_blockchain_transaction(tx_hash) 610 | _ec, rawtx = await self.bx.fetch_mempool_transaction(tx_hash) 611 | if _ec and _ec != ZMQError.success and _ec != ZMQError.not_found: 612 | self.log.error("fetch_mempool_transaction: %s", _ec.name) 613 | return JsonRPCError.internalerror() 614 | 615 | # Behaviour is undefined in spec 616 | if not rawtx: 617 | return JsonRPCError.internalerror() 618 | # return {"result": None} 619 | 620 | if verbose: 621 | # TODO: Help needed 622 | return JsonRPCError.invalidrequest() 623 | 624 | return {"result": bh2u(rawtx)} 625 | 626 | async def transaction_get_merkle(self, writer, query): # pylint: disable=W0613 627 | """Method: blockchain.transaction.get_merkle 628 | Return the merkle branch to a confirmed transaction given its 629 | hash and height. 630 | """ 631 | if "params" not in query or len(query["params"]) != 2: 632 | return JsonRPCError.invalidparams() 633 | 634 | tx_hash = query["params"][0] 635 | height = query["params"][1] 636 | 637 | if not is_hash256_str(tx_hash): 638 | return JsonRPCError.invalidparams() 639 | if not is_non_negative_integer(height): 640 | return JsonRPCError.invalidparams() 641 | 642 | _ec, hashes = await self.bx.fetch_block_transaction_hashes(height) 643 | if _ec and _ec != ZMQError.success: 644 | self.log.error("bx.fetch_block_transaction_hashes: %s", _ec.name) 645 | return JsonRPCError.internalerror() 646 | 647 | # Decouple from tuples 648 | hashes = [i[0] for i in hashes] 649 | tx_pos = hashes.index(unhexlify(tx_hash)[::-1]) 650 | branch = merkle_branch(hashes, tx_pos) 651 | 652 | res = { 653 | "block_height": int(height), 654 | "pos": int(tx_pos), 655 | "merkle": branch, 656 | } 657 | return {"result": res} 658 | 659 | async def transaction_id_from_pos(self, writer, query): # pylint: disable=R0911,W0613 660 | """Method: blockchain.transaction.id_from_pos 661 | Return a transaction hash and optionally a merkle proof, given a 662 | block height and a position in the block. 663 | """ 664 | if "params" not in query or len(query["params"]) < 2: 665 | return JsonRPCError.invalidparams() 666 | 667 | height = query["params"][0] 668 | tx_pos = query["params"][1] 669 | merkle = query["params"][2] if len(query["params"]) > 2 else False 670 | 671 | if not is_non_negative_integer(height): 672 | return JsonRPCError.invalidparams() 673 | if not is_non_negative_integer(tx_pos): 674 | return JsonRPCError.invalidparams() 675 | if not is_boolean(merkle): 676 | return JsonRPCError.invalidparams() 677 | 678 | _ec, hashes = await self.bx.fetch_block_transaction_hashes(height) 679 | if _ec and _ec != ZMQError.success: 680 | self.log.error("bx.fetch_block_transaction_hashes: %s", _ec.name) 681 | return JsonRPCError.internalerror() 682 | 683 | if len(hashes) - 1 < tx_pos: 684 | return JsonRPCError.internalerror() 685 | 686 | # Decouple from tuples 687 | hashes = [i[0] for i in hashes] 688 | txid = hash_to_hex_str(hashes[tx_pos]) 689 | 690 | if not merkle: 691 | return {"result": txid} 692 | 693 | branch = merkle_branch(hashes, tx_pos) 694 | return {"result": {"tx_hash": txid, "merkle": branch}} 695 | 696 | async def get_fee_histogram(self, writer, query): # pylint: disable=W0613 697 | """Method: mempool.get_fee_histogram 698 | Return a histogram of the fee rates paid by transactions in the 699 | memory pool, weighted by transaction size. 700 | """ 701 | # NOTE: This solution is using the mempool.space API. 702 | # Let's try to eventually solve it with some internal logic. 703 | if self.chain == "testnet": 704 | return {"result": [[0, 0]]} 705 | 706 | fee_hist = get_fee_histogram() 707 | if not fee_dict: 708 | return {"result": [[0, 0]]} 709 | 710 | return {"result": fee_hist} 711 | 712 | async def add_peer(self, writer, query): # pylint: disable=W0613 713 | """Method: server.add_peer 714 | A newly-started server uses this call to get itself into other 715 | servers’ peers lists. It should not be used by wallet clients. 716 | """ 717 | # TODO: Help wanted 718 | return {"result": False} 719 | 720 | async def banner(self, writer, query): # pylint: disable=W0613 721 | """Method: server.banner 722 | Return a banner to be shown in the Electrum console. 723 | """ 724 | _, bsversion = await self.bx.server_version() 725 | banner = "%s\nobelisk version: %s\nlibbitcoin-server version: %s" % ( 726 | BANNER, 727 | VERSION, 728 | bsversion.decode(), 729 | ) 730 | return {"result": banner} 731 | 732 | async def donation_address(self, writer, query): # pylint: disable=W0613 733 | """Method: server.donation_address 734 | Return a server donation address. 735 | """ 736 | return {"result": DONATION_ADDR} 737 | 738 | async def server_features(self, writer, query): # pylint: disable=W0613 # pragma: no cover 739 | """Method: server.features 740 | Return a list of features and services supported by the server. 741 | """ 742 | cfg = self.server_cfg 743 | hosts = {} 744 | for host in cfg["server_hostnames"]: 745 | hosts[host] = {"tcp_port": cfg["server_port"]} 746 | 747 | return { 748 | "result": { 749 | "genesis_hash": self.genesis, 750 | "hosts": hosts, 751 | "protocol_max": SERVER_PROTO_MAX, 752 | "protocol_min": SERVER_PROTO_MIN, 753 | "pruning": None, 754 | "server_version": f"obelisk {VERSION}", 755 | "hash_function": "sha256", 756 | } 757 | } 758 | 759 | async def peers_subscribe(self, writer, query): # pylint: disable=W0613 760 | """Method: server.peers.subscribe 761 | Return a list of peer servers. Despite the name this is not a 762 | subscription and the server must send no notifications. 763 | """ 764 | # TODO: Help wanted 765 | return {"result": []} 766 | 767 | async def ping(self, writer, query): # pylint: disable=W0613 768 | """Method: server.ping 769 | Ping the server to ensure it is responding, and to keep the session 770 | alive. The server may disconnect clients that have sent no requests 771 | for roughly 10 minutes. 772 | """ 773 | return {"result": None} 774 | 775 | async def server_version(self, writer, query): # pylint: disable=W0613 776 | """Method: server.version 777 | Identify the client to the server and negotiate the protocol version. 778 | """ 779 | if "params" not in query or len(query["params"]) != 2: 780 | return JsonRPCError.invalidparams() 781 | 782 | client_ver = query["params"][1] 783 | 784 | if isinstance(client_ver, list): 785 | client_min, client_max = client_ver[0], client_ver[1] 786 | else: 787 | client_min = client_max = client_ver 788 | 789 | version = min(client_max, SERVER_PROTO_MAX) 790 | 791 | if version < max(client_min, SERVER_PROTO_MIN): 792 | return JsonRPCError.protonotsupported() 793 | 794 | return {"result": [f"obelisk {VERSION}", version]} 795 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------