Browse Source

Extend copyright notice; improve comments

master
Neil Booth 8 years ago
parent
commit
d2ebb80fac
  1. 11
      electrumx_rpc.py
  2. 48
      lib/coins.py
  3. 23
      lib/enum.py
  4. 44
      lib/hash.py
  5. 25
      lib/script.py
  6. 15
      lib/tx.py
  7. 10
      lib/util.py
  8. 14
      query.py
  9. 9
      server/block_processor.py
  10. 13
      server/cache.py
  11. 12
      server/controller.py
  12. 8
      server/daemon.py
  13. 9
      server/env.py
  14. 9
      server/protocol.py
  15. 11
      server_main.py

11
electrumx_rpc.py

@ -1,8 +1,15 @@
#!/usr/bin/env python3 #!/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. # and warranty status of this software.
'''Script to send RPC commands to a running ElectrumX server.'''
import argparse import argparse
import asyncio import asyncio
import json import json

48
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. # 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 from decimal import Decimal
import inspect import inspect
@ -12,7 +21,7 @@ from lib.tx import Deserializer
class CoinError(Exception): class CoinError(Exception):
pass '''Exception raised for coin-related errors.'''
class Coin(object): class Coin(object):
@ -24,17 +33,20 @@ class Coin(object):
VALUE_PER_COIN = 100000000 VALUE_PER_COIN = 100000000
@staticmethod @staticmethod
def coins(): def coin_classes():
'''Return a list of coin classes in declaration order.'''
is_coin = lambda obj: (inspect.isclass(obj) is_coin = lambda obj: (inspect.isclass(obj)
and issubclass(obj, Coin) and issubclass(obj, Coin)
and obj != Coin) and obj != Coin)
pairs = inspect.getmembers(sys.modules[__name__], is_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] return [pair[1] for pair in pairs]
@classmethod @classmethod
def lookup_coin_class(cls, name, net): 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() if (coin.NAME.lower() == name.lower()
and coin.NET.lower() == net.lower()): and coin.NET.lower() == net.lower()):
return coin return coin
@ -43,13 +55,14 @@ class Coin(object):
@staticmethod @staticmethod
def lookup_xverbytes(verbytes): def lookup_xverbytes(verbytes):
'''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.'''
# Order means BTC testnet will override NMC testnet # Order means BTC testnet will override NMC testnet
for coin in Coin.coins(): for coin in Coin.coin_classes():
if verbytes == coin.XPUB_VERBYTES: if verbytes == coin.XPUB_VERBYTES:
return True, coin return True, coin
if verbytes == coin.XPRV_VERBYTES: if verbytes == coin.XPRV_VERBYTES:
return False, coin return False, coin
raise CoinError("version bytes unrecognised") raise CoinError('version bytes unrecognised')
@classmethod @classmethod
def address_to_hash168(cls, addr): def address_to_hash168(cls, addr):
@ -129,7 +142,7 @@ class Coin(object):
@classmethod @classmethod
def prvkey_WIF(privkey_bytes, compressed): 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 payload = bytearray([cls.WIF_BYTE]) + privkey_bytes
if compressed: if compressed:
payload.append(0x01) payload.append(0x01)
@ -137,18 +150,22 @@ class Coin(object):
@classmethod @classmethod
def header_hashes(cls, header): def header_hashes(cls, header):
'''Given a header return the previous block hash and the current block '''Given a header return the previous and current block hashes.'''
hash.'''
return header[4:36], double_sha256(header) return header[4:36], double_sha256(header)
@classmethod @classmethod
def read_block(cls, block): 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:] header, rest = block[:cls.HEADER_LEN], block[cls.HEADER_LEN:]
return (header, ) + Deserializer(rest).read_block() return (header, ) + Deserializer(rest).read_block()
@classmethod @classmethod
def decimal_value(cls, value): 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 return Decimal(value) / cls.VALUE_PER_COIN
@ -167,6 +184,7 @@ class Bitcoin(Coin):
TX_COUNT_HEIGHT = 420976 TX_COUNT_HEIGHT = 420976
TX_PER_BLOCK = 1600 TX_PER_BLOCK = 1600
class BitcoinTestnet(Coin): class BitcoinTestnet(Coin):
NAME = "Bitcoin" NAME = "Bitcoin"
SHORTNAME = "XTN" SHORTNAME = "XTN"
@ -177,6 +195,7 @@ class BitcoinTestnet(Coin):
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
# Source: pycoin and others # Source: pycoin and others
class Litecoin(Coin): class Litecoin(Coin):
NAME = "Litecoin" NAME = "Litecoin"
@ -188,6 +207,7 @@ class Litecoin(Coin):
P2SH_VERBYTE = 0x05 P2SH_VERBYTE = 0x05
WIF_BYTE = 0xb0 WIF_BYTE = 0xb0
class LitecoinTestnet(Coin): class LitecoinTestnet(Coin):
NAME = "Litecoin" NAME = "Litecoin"
SHORTNAME = "XLT" SHORTNAME = "XLT"
@ -198,6 +218,7 @@ class LitecoinTestnet(Coin):
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
# Source: namecoin.org # Source: namecoin.org
class Namecoin(Coin): class Namecoin(Coin):
NAME = "Namecoin" NAME = "Namecoin"
@ -209,6 +230,7 @@ class Namecoin(Coin):
P2SH_VERBYTE = 0x0d P2SH_VERBYTE = 0x0d
WIF_BYTE = 0xe4 WIF_BYTE = 0xe4
class NamecoinTestnet(Coin): class NamecoinTestnet(Coin):
NAME = "Namecoin" NAME = "Namecoin"
SHORTNAME = "XNM" SHORTNAME = "XNM"
@ -219,6 +241,7 @@ class NamecoinTestnet(Coin):
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
# For DOGE there is disagreement across sites like bip32.org and # For DOGE there is disagreement across sites like bip32.org and
# pycoin. Taken from bip32.org and bitmerchant on github # pycoin. Taken from bip32.org and bitmerchant on github
class Dogecoin(Coin): class Dogecoin(Coin):
@ -231,6 +254,7 @@ class Dogecoin(Coin):
P2SH_VERBYTE = 0x16 P2SH_VERBYTE = 0x16
WIF_BYTE = 0x9e WIF_BYTE = 0x9e
class DogecoinTestnet(Coin): class DogecoinTestnet(Coin):
NAME = "Dogecoin" NAME = "Dogecoin"
SHORTNAME = "XDT" SHORTNAME = "XDT"
@ -241,6 +265,7 @@ class DogecoinTestnet(Coin):
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xf1 WIF_BYTE = 0xf1
# Source: pycoin # Source: pycoin
class Dash(Coin): class Dash(Coin):
NAME = "Dash" NAME = "Dash"
@ -252,6 +277,7 @@ class Dash(Coin):
P2SH_VERBYTE = 0x10 P2SH_VERBYTE = 0x10
WIF_BYTE = 0xcc WIF_BYTE = 0xcc
class DashTestnet(Coin): class DashTestnet(Coin):
NAME = "Dogecoin" NAME = "Dogecoin"
SHORTNAME = "tDASH" SHORTNAME = "tDASH"

23
lib/enum.py

@ -1,8 +1,17 @@
# enum-like type # Copyright (c) 2016, Neil Booth
# From the Python Cookbook from http://code.activestate.com/recipes/67107/ #
# 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 pass
@ -20,13 +29,13 @@ class Enumeration:
if isinstance(x, tuple): if isinstance(x, tuple):
x, i = x x, i = x
if not isinstance(x, str): 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): 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: if x in uniqueNames:
raise EnumException("enum name {} not unique".format(x)) raise EnumError("enum name {} not unique".format(x))
if i in uniqueValues: if i in uniqueValues:
raise EnumException("enum value {} not unique".format(x)) raise EnumError("enum value {} not unique".format(x))
uniqueNames.add(x) uniqueNames.add(x)
uniqueValues.add(i) uniqueValues.add(i)
lookup[x] = i lookup[x] = i

44
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. # and warranty status of this software.
'''Cryptograph hash functions and related classes.'''
import hashlib import hashlib
import hmac import hmac
@ -8,11 +15,13 @@ from lib.util import bytes_to_int, int_to_bytes
def sha256(x): def sha256(x):
'''Simple wrapper of hashlib sha256.'''
assert isinstance(x, (bytes, bytearray, memoryview)) assert isinstance(x, (bytes, bytearray, memoryview))
return hashlib.sha256(x).digest() return hashlib.sha256(x).digest()
def ripemd160(x): def ripemd160(x):
'''Simple wrapper of hashlib ripemd160.'''
assert isinstance(x, (bytes, bytearray, memoryview)) assert isinstance(x, (bytes, bytearray, memoryview))
h = hashlib.new('ripemd160') h = hashlib.new('ripemd160')
h.update(x) h.update(x)
@ -20,36 +29,41 @@ def ripemd160(x):
def double_sha256(x): def double_sha256(x):
'''SHA-256 of SHA-256, as used extensively in bitcoin.'''
return sha256(sha256(x)) return sha256(sha256(x))
def hmac_sha512(key, msg): def hmac_sha512(key, msg):
'''Use SHA-512 to provide an HMAC.'''
return hmac.new(key, msg, hashlib.sha512).digest() return hmac.new(key, msg, hashlib.sha512).digest()
def hash160(x): def hash160(x):
'''RIPEMD-160 of SHA-256.
Used to make bitcoin addresses from pubkeys.'''
return ripemd160(sha256(x)) return ripemd160(sha256(x))
def hash_to_str(x): def hash_to_str(x):
'''Converts a big-endian binary hash to a little-endian hex string, as '''Convert a big-endian binary hash to displayed hex string.
shown in block explorers, etc.
Display form of a binary hash is reversed and converted to hex.
''' '''
return bytes(reversed(x)).hex() return bytes(reversed(x)).hex()
def hex_str_to_hash(x): def hex_str_to_hash(x):
'''Converts a little-endian hex string as shown to a big-endian binary '''Convert a displayed hex string to a binary hash.'''
hash.'''
return bytes(reversed(bytes.fromhex(x))) return bytes(reversed(bytes.fromhex(x)))
class InvalidBase58String(Exception):
pass
class InvalidBase58CheckSum(Exception): class Base58Error(Exception):
pass '''Exception used for Base58 errors.'''
class Base58(object): class Base58(object):
'''Class providing base 58 functionality.'''
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
assert len(chars) == 58 assert len(chars) == 58
@ -59,17 +73,17 @@ class Base58(object):
def char_value(c): def char_value(c):
val = Base58.cmap.get(c) val = Base58.cmap.get(c)
if val is None: if val is None:
raise InvalidBase58String raise Base58Error('invalid base 58 character "{}"'.format(c))
return val return val
@staticmethod @staticmethod
def decode(txt): def decode(txt):
"""Decodes txt into a big-endian bytearray.""" """Decodes txt into a big-endian bytearray."""
if not isinstance(txt, str): if not isinstance(txt, str):
raise InvalidBase58String("a string is required") raise Base58Error('a string is required')
if not txt: if not txt:
raise InvalidBase58String("string cannot be empty") raise Base58Error('string cannot be empty')
value = 0 value = 0
for c in txt: for c in txt:
@ -112,14 +126,14 @@ class Base58(object):
be_bytes = Base58.decode(txt) be_bytes = Base58.decode(txt)
result, check = be_bytes[:-4], be_bytes[-4:] result, check = be_bytes[:-4], be_bytes[-4:]
if check != double_sha256(result)[:4]: if check != double_sha256(result)[:4]:
raise InvalidBase58CheckSum raise Base58Error('invalid base 58 checksum for {}'.format(txt))
return result return result
@staticmethod @staticmethod
def encode_check(payload): def encode_check(payload):
"""Encodes a payload bytearray (which includes the version byte(s)) """Encodes a payload bytearray (which includes the version byte(s))
into a Base58Check string.""" into a Base58Check string."""
assert isinstance(payload, (bytes, bytearray)) assert isinstance(payload, (bytes, bytearray, memoryview))
be_bytes = payload + double_sha256(payload)[:4] be_bytes = payload + double_sha256(payload)[:4]
return Base58.encode(be_bytes) return Base58.encode(be_bytes)

25
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. # and warranty status of this software.
'''Script-related classes and functions.'''
from binascii import hexlify from binascii import hexlify
import struct import struct
@ -10,7 +17,7 @@ from lib.util import cachedproperty
class ScriptError(Exception): class ScriptError(Exception):
pass '''Exception used for script errors.'''
OpCodes = Enumeration("Opcodes", [ OpCodes = Enumeration("Opcodes", [
@ -52,7 +59,9 @@ assert OpCodes.OP_CHECKMULTISIG == 0xae
class ScriptSig(object): 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) SIG_ADDRESS, SIG_MULTI, SIG_PUBKEY, SIG_UNKNOWN = range(4)
@ -73,8 +82,9 @@ class ScriptSig(object):
@classmethod @classmethod
def from_script(cls, script, coin): def from_script(cls, script, coin):
'''Returns an instance of this class. Uncrecognised scripts return '''Return an instance of this class.
an object of kind SIG_UNKNOWN.'''
Return an object with kind SIG_UNKNOWN for unrecognised scripts.'''
try: try:
return cls.parse_script(script, coin) return cls.parse_script(script, coin)
except ScriptError: except ScriptError:
@ -82,8 +92,9 @@ class ScriptSig(object):
@classmethod @classmethod
def parse_script(cls, script, coin): def parse_script(cls, script, coin):
'''Returns an instance of this class. Raises on unrecognised '''Return an instance of this class.
scripts.'''
Raises on unrecognised scripts.'''
ops, datas = Script.get_ops(script) ops, datas = Script.get_ops(script)
# Address, PubKey and P2SH redeems only push data # Address, PubKey and P2SH redeems only push data

15
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. # and warranty status of this software.
'''Transaction-related classes and functions.'''
from collections import namedtuple from collections import namedtuple
import struct import struct
@ -9,6 +16,7 @@ from lib.hash import double_sha256, hash_to_str
class Tx(namedtuple("Tx", "version inputs outputs locktime")): class Tx(namedtuple("Tx", "version inputs outputs locktime")):
'''Class representing a transaction.'''
@cachedproperty @cachedproperty
def is_coinbase(self): def is_coinbase(self):
@ -17,6 +25,7 @@ class Tx(namedtuple("Tx", "version inputs outputs locktime")):
# FIXME: add hash as a cached property? # FIXME: add hash as a cached property?
class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")): class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")):
'''Class representing a transaction input.'''
ZERO = bytes(32) ZERO = bytes(32)
MINUS_1 = 4294967295 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 TxOutput(namedtuple("TxOutput", "value pk_script")):
'''Class representing a transaction output.'''
@cachedproperty @cachedproperty
def pay_to(self): def pay_to(self):
@ -48,9 +58,10 @@ class TxOutput(namedtuple("TxOutput", "value pk_script")):
class Deserializer(object): class Deserializer(object):
'''Deserializes blocks into transactions.'''
def __init__(self, binary): def __init__(self, binary):
assert isinstance(binary, (bytes, memoryview)) assert isinstance(binary, bytes)
self.binary = binary self.binary = binary
self.cursor = 0 self.cursor = 0

10
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. # and warranty status of this software.
'''Miscellaneous utility classes and functions.'''
import array import array
import logging import logging
import sys import sys
@ -72,6 +79,7 @@ def deep_getsizeof(obj):
def chunks(items, size): def chunks(items, size):
'''Break up items, an iterable, into chunks of length size.'''
for i in range(0, len(items), size): for i in range(0, len(items), size):
yield items[i: i + size] yield items[i: i + size]

14
query.py

@ -1,8 +1,18 @@
#!/usr/bin/env python3 #!/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. # 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 os
import sys import sys

9
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. # and warranty status of this software.
'''Block prefetcher and chain processor.'''
import array import array
import ast import ast
import asyncio import asyncio

13
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. # 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 array
import itertools import itertools
import os import os

12
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. # 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 asyncio
import signal import signal
import traceback import traceback

8
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. # and warranty status of this software.
'''Classes for handling asynchronous connections to a blockchain '''Class for handling asynchronous connections to a blockchain
daemon.''' daemon.'''
import asyncio import asyncio

9
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. # and warranty status of this software.
'''Class for handling environment configuration and defaults.'''
from os import environ from os import environ
from lib.coins import Coin from lib.coins import Coin

9
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. # and warranty status of this software.
'''Classes for local RPC server and remote client TCP/SSL servers.'''
import asyncio import asyncio
import codecs import codecs
import json import json

11
server_main.py

@ -1,8 +1,15 @@
#!/usr/bin/env python3 #!/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. # and warranty status of this software.
'''Script to kick off the server.'''
import asyncio import asyncio
import logging import logging
import os import os

Loading…
Cancel
Save