diff --git a/electrumx_rpc.py b/electrumx_rpc.py index 68fc74c..2ce4dee 100755 --- a/electrumx_rpc.py +++ b/electrumx_rpc.py @@ -1,8 +1,15 @@ #!/usr/bin/env python3 - -# See the file "LICENSE" for information about the copyright +# +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Script to send RPC commands to a running ElectrumX server.''' + + import argparse import asyncio import json diff --git a/lib/coins.py b/lib/coins.py index 30a0b12..6537e74 100644 --- a/lib/coins.py +++ b/lib/coins.py @@ -1,6 +1,15 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Module providing coin abstraction. + +Anything coin-specific should go in this file and be subclassed where +necessary for appropriate handling. +''' from decimal import Decimal import inspect @@ -12,7 +21,7 @@ from lib.tx import Deserializer class CoinError(Exception): - pass + '''Exception raised for coin-related errors.''' class Coin(object): @@ -24,17 +33,20 @@ class Coin(object): VALUE_PER_COIN = 100000000 @staticmethod - def coins(): + def coin_classes(): + '''Return a list of coin classes in declaration order.''' is_coin = lambda obj: (inspect.isclass(obj) and issubclass(obj, Coin) and obj != Coin) pairs = inspect.getmembers(sys.modules[__name__], is_coin) - # Returned in the order they appear in this file return [pair[1] for pair in pairs] @classmethod def lookup_coin_class(cls, name, net): - for coin in cls.coins(): + '''Return a coin class given name and network. + + Raise an exception if unrecognised.''' + for coin in cls.coin_classes(): if (coin.NAME.lower() == name.lower() and coin.NET.lower() == net.lower()): return coin @@ -43,13 +55,14 @@ class Coin(object): @staticmethod def lookup_xverbytes(verbytes): + '''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.''' # Order means BTC testnet will override NMC testnet - for coin in Coin.coins(): + for coin in Coin.coin_classes(): if verbytes == coin.XPUB_VERBYTES: return True, coin if verbytes == coin.XPRV_VERBYTES: return False, coin - raise CoinError("version bytes unrecognised") + raise CoinError('version bytes unrecognised') @classmethod def address_to_hash168(cls, addr): @@ -129,7 +142,7 @@ class Coin(object): @classmethod def prvkey_WIF(privkey_bytes, compressed): - "Return the private key encoded in Wallet Import Format." + '''Return the private key encoded in Wallet Import Format.''' payload = bytearray([cls.WIF_BYTE]) + privkey_bytes if compressed: payload.append(0x01) @@ -137,18 +150,22 @@ class Coin(object): @classmethod def header_hashes(cls, header): - '''Given a header return the previous block hash and the current block - hash.''' + '''Given a header return the previous and current block hashes.''' return header[4:36], double_sha256(header) @classmethod def read_block(cls, block): - '''Read a block and return (header, tx_hashes, txs)''' + '''Return a tuple (header, tx_hashes, txs) given a raw block.''' header, rest = block[:cls.HEADER_LEN], block[cls.HEADER_LEN:] return (header, ) + Deserializer(rest).read_block() @classmethod def decimal_value(cls, value): + '''Return the number of standard coin units as a Decimal given a + quantity of smallest units. + + For example 1 BTC is returned for 100 million satoshis. + ''' return Decimal(value) / cls.VALUE_PER_COIN @@ -167,6 +184,7 @@ class Bitcoin(Coin): TX_COUNT_HEIGHT = 420976 TX_PER_BLOCK = 1600 + class BitcoinTestnet(Coin): NAME = "Bitcoin" SHORTNAME = "XTN" @@ -177,6 +195,7 @@ class BitcoinTestnet(Coin): P2SH_VERBYTE = 0xc4 WIF_BYTE = 0xef + # Source: pycoin and others class Litecoin(Coin): NAME = "Litecoin" @@ -188,6 +207,7 @@ class Litecoin(Coin): P2SH_VERBYTE = 0x05 WIF_BYTE = 0xb0 + class LitecoinTestnet(Coin): NAME = "Litecoin" SHORTNAME = "XLT" @@ -198,6 +218,7 @@ class LitecoinTestnet(Coin): P2SH_VERBYTE = 0xc4 WIF_BYTE = 0xef + # Source: namecoin.org class Namecoin(Coin): NAME = "Namecoin" @@ -209,6 +230,7 @@ class Namecoin(Coin): P2SH_VERBYTE = 0x0d WIF_BYTE = 0xe4 + class NamecoinTestnet(Coin): NAME = "Namecoin" SHORTNAME = "XNM" @@ -219,6 +241,7 @@ class NamecoinTestnet(Coin): P2SH_VERBYTE = 0xc4 WIF_BYTE = 0xef + # For DOGE there is disagreement across sites like bip32.org and # pycoin. Taken from bip32.org and bitmerchant on github class Dogecoin(Coin): @@ -231,6 +254,7 @@ class Dogecoin(Coin): P2SH_VERBYTE = 0x16 WIF_BYTE = 0x9e + class DogecoinTestnet(Coin): NAME = "Dogecoin" SHORTNAME = "XDT" @@ -241,6 +265,7 @@ class DogecoinTestnet(Coin): P2SH_VERBYTE = 0xc4 WIF_BYTE = 0xf1 + # Source: pycoin class Dash(Coin): NAME = "Dash" @@ -252,6 +277,7 @@ class Dash(Coin): P2SH_VERBYTE = 0x10 WIF_BYTE = 0xcc + class DashTestnet(Coin): NAME = "Dogecoin" SHORTNAME = "tDASH" diff --git a/lib/enum.py b/lib/enum.py index e137c7f..920e038 100644 --- a/lib/enum.py +++ b/lib/enum.py @@ -1,8 +1,17 @@ -# enum-like type -# From the Python Cookbook from http://code.activestate.com/recipes/67107/ +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright +# and warranty status of this software. +'''An enum-like type with reverse lookup. -class EnumException(Exception): +Source: Python Cookbook, http://code.activestate.com/recipes/67107/ +''' + + +class EnumError(Exception): pass @@ -20,13 +29,13 @@ class Enumeration: if isinstance(x, tuple): x, i = x if not isinstance(x, str): - raise EnumException("enum name {} not a string".format(x)) + raise EnumError("enum name {} not a string".format(x)) if not isinstance(i, int): - raise EnumException("enum value {} not an integer".format(i)) + raise EnumError("enum value {} not an integer".format(i)) if x in uniqueNames: - raise EnumException("enum name {} not unique".format(x)) + raise EnumError("enum name {} not unique".format(x)) if i in uniqueValues: - raise EnumException("enum value {} not unique".format(x)) + raise EnumError("enum value {} not unique".format(x)) uniqueNames.add(x) uniqueValues.add(i) lookup[x] = i diff --git a/lib/hash.py b/lib/hash.py index bf0aed6..1563adc 100644 --- a/lib/hash.py +++ b/lib/hash.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Cryptograph hash functions and related classes.''' + + import hashlib import hmac @@ -8,11 +15,13 @@ from lib.util import bytes_to_int, int_to_bytes def sha256(x): + '''Simple wrapper of hashlib sha256.''' assert isinstance(x, (bytes, bytearray, memoryview)) return hashlib.sha256(x).digest() def ripemd160(x): + '''Simple wrapper of hashlib ripemd160.''' assert isinstance(x, (bytes, bytearray, memoryview)) h = hashlib.new('ripemd160') h.update(x) @@ -20,36 +29,41 @@ def ripemd160(x): def double_sha256(x): + '''SHA-256 of SHA-256, as used extensively in bitcoin.''' return sha256(sha256(x)) def hmac_sha512(key, msg): + '''Use SHA-512 to provide an HMAC.''' return hmac.new(key, msg, hashlib.sha512).digest() def hash160(x): + '''RIPEMD-160 of SHA-256. + + Used to make bitcoin addresses from pubkeys.''' return ripemd160(sha256(x)) + def hash_to_str(x): - '''Converts a big-endian binary hash to a little-endian hex string, as - shown in block explorers, etc. + '''Convert a big-endian binary hash to displayed hex string. + + Display form of a binary hash is reversed and converted to hex. ''' return bytes(reversed(x)).hex() + def hex_str_to_hash(x): - '''Converts a little-endian hex string as shown to a big-endian binary - hash.''' + '''Convert a displayed hex string to a binary hash.''' return bytes(reversed(bytes.fromhex(x))) -class InvalidBase58String(Exception): - pass - -class InvalidBase58CheckSum(Exception): - pass +class Base58Error(Exception): + '''Exception used for Base58 errors.''' class Base58(object): + '''Class providing base 58 functionality.''' chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' assert len(chars) == 58 @@ -59,17 +73,17 @@ class Base58(object): def char_value(c): val = Base58.cmap.get(c) if val is None: - raise InvalidBase58String + raise Base58Error('invalid base 58 character "{}"'.format(c)) return val @staticmethod def decode(txt): """Decodes txt into a big-endian bytearray.""" if not isinstance(txt, str): - raise InvalidBase58String("a string is required") + raise Base58Error('a string is required') if not txt: - raise InvalidBase58String("string cannot be empty") + raise Base58Error('string cannot be empty') value = 0 for c in txt: @@ -112,14 +126,14 @@ class Base58(object): be_bytes = Base58.decode(txt) result, check = be_bytes[:-4], be_bytes[-4:] if check != double_sha256(result)[:4]: - raise InvalidBase58CheckSum + raise Base58Error('invalid base 58 checksum for {}'.format(txt)) return result @staticmethod def encode_check(payload): """Encodes a payload bytearray (which includes the version byte(s)) into a Base58Check string.""" - assert isinstance(payload, (bytes, bytearray)) + assert isinstance(payload, (bytes, bytearray, memoryview)) be_bytes = payload + double_sha256(payload)[:4] return Base58.encode(be_bytes) diff --git a/lib/script.py b/lib/script.py index 0239a1d..3894892 100644 --- a/lib/script.py +++ b/lib/script.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Script-related classes and functions.''' + + from binascii import hexlify import struct @@ -10,7 +17,7 @@ from lib.util import cachedproperty class ScriptError(Exception): - pass + '''Exception used for script errors.''' OpCodes = Enumeration("Opcodes", [ @@ -52,7 +59,9 @@ assert OpCodes.OP_CHECKMULTISIG == 0xae class ScriptSig(object): - '''A script from a tx input, typically provides one or more signatures.''' + '''A script from a tx input. + + Typically provides one or more signatures.''' SIG_ADDRESS, SIG_MULTI, SIG_PUBKEY, SIG_UNKNOWN = range(4) @@ -73,8 +82,9 @@ class ScriptSig(object): @classmethod def from_script(cls, script, coin): - '''Returns an instance of this class. Uncrecognised scripts return - an object of kind SIG_UNKNOWN.''' + '''Return an instance of this class. + + Return an object with kind SIG_UNKNOWN for unrecognised scripts.''' try: return cls.parse_script(script, coin) except ScriptError: @@ -82,8 +92,9 @@ class ScriptSig(object): @classmethod def parse_script(cls, script, coin): - '''Returns an instance of this class. Raises on unrecognised - scripts.''' + '''Return an instance of this class. + + Raises on unrecognised scripts.''' ops, datas = Script.get_ops(script) # Address, PubKey and P2SH redeems only push data diff --git a/lib/tx.py b/lib/tx.py index c45425c..7ea35f4 100644 --- a/lib/tx.py +++ b/lib/tx.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Transaction-related classes and functions.''' + + from collections import namedtuple import struct @@ -9,6 +16,7 @@ from lib.hash import double_sha256, hash_to_str class Tx(namedtuple("Tx", "version inputs outputs locktime")): + '''Class representing a transaction.''' @cachedproperty def is_coinbase(self): @@ -17,6 +25,7 @@ class Tx(namedtuple("Tx", "version inputs outputs locktime")): # FIXME: add hash as a cached property? class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")): + '''Class representing a transaction input.''' ZERO = bytes(32) MINUS_1 = 4294967295 @@ -41,6 +50,7 @@ class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")): class TxOutput(namedtuple("TxOutput", "value pk_script")): + '''Class representing a transaction output.''' @cachedproperty def pay_to(self): @@ -48,9 +58,10 @@ class TxOutput(namedtuple("TxOutput", "value pk_script")): class Deserializer(object): + '''Deserializes blocks into transactions.''' def __init__(self, binary): - assert isinstance(binary, (bytes, memoryview)) + assert isinstance(binary, bytes) self.binary = binary self.cursor = 0 diff --git a/lib/util.py b/lib/util.py index 552a352..f59439b 100644 --- a/lib/util.py +++ b/lib/util.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Miscellaneous utility classes and functions.''' + + import array import logging import sys @@ -72,6 +79,7 @@ def deep_getsizeof(obj): def chunks(items, size): + '''Break up items, an iterable, into chunks of length size.''' for i in range(0, len(items), size): yield items[i: i + size] diff --git a/query.py b/query.py index 57941fa..faea87a 100755 --- a/query.py +++ b/query.py @@ -1,8 +1,18 @@ #!/usr/bin/env python3 - -# See the file "LICENSE" for information about the copyright +# +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Script to query the database for debugging purposes. + +Not currently documented; might become easier to use in future. +''' + + import os import sys diff --git a/server/block_processor.py b/server/block_processor.py index aeaade9..ba42a0e 100644 --- a/server/block_processor.py +++ b/server/block_processor.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Block prefetcher and chain processor.''' + + import array import ast import asyncio diff --git a/server/cache.py b/server/cache.py index 0f0cc71..3adfbe8 100644 --- a/server/cache.py +++ b/server/cache.py @@ -1,6 +1,17 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''UTXO and file cache. + +During initial sync these cache data and only flush occasionally. +Once synced flushes are performed after processing each block. +''' + + import array import itertools import os diff --git a/server/controller.py b/server/controller.py index 5812f04..9d1cf64 100644 --- a/server/controller.py +++ b/server/controller.py @@ -1,6 +1,16 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Server controller. + +Coordinates the parts of the server. Serves as a cache for +client-serving data such as histories. +''' + import asyncio import signal import traceback diff --git a/server/daemon.py b/server/daemon.py index 96a07bc..aaef6c1 100644 --- a/server/daemon.py +++ b/server/daemon.py @@ -1,7 +1,11 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Classes for handling asynchronous connections to a blockchain +'''Class for handling asynchronous connections to a blockchain daemon.''' import asyncio diff --git a/server/env.py b/server/env.py index 0a447ed..e27f570 100644 --- a/server/env.py +++ b/server/env.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Class for handling environment configuration and defaults.''' + + from os import environ from lib.coins import Coin diff --git a/server/protocol.py b/server/protocol.py index 04b4dcf..ebd8df7 100644 --- a/server/protocol.py +++ b/server/protocol.py @@ -1,6 +1,13 @@ -# See the file "LICENSE" for information about the copyright +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Classes for local RPC server and remote client TCP/SSL servers.''' + + import asyncio import codecs import json diff --git a/server_main.py b/server_main.py index 3a82c48..60948fa 100755 --- a/server_main.py +++ b/server_main.py @@ -1,8 +1,15 @@ #!/usr/bin/env python3 - -# See the file "LICENSE" for information about the copyright +# +# Copyright (c) 2016, Neil Booth +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright # and warranty status of this software. +'''Script to kick off the server.''' + + import asyncio import logging import os