# 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 from functools import partial import inspect import re import struct import sys from lib.hash import Base58, hash160, double_sha256, hash_to_str from lib.script import ScriptPubKey, Script from lib.tx import Deserializer from lib.util import cachedproperty, subclasses class CoinError(Exception): '''Exception raised for coin-related errors.''' class Coin(object): '''Base class of coin hierarchy.''' REORG_LIMIT=200 # Not sure if these are coin-specific HEADER_LEN = 80 RPC_URL_REGEX = re.compile('.+@[^:]+(:[0-9]+)?') VALUE_PER_COIN = 100000000 CHUNK_SIZE=2016 STRANGE_VERBYTE = 0xff IRC_SERVER = "irc.freenode.net" IRC_PORT = 6667 @classmethod def lookup_coin_class(cls, name, net): '''Return a coin class given name and network. Raise an exception if unrecognised.''' for coin in subclasses(Coin): if (coin.NAME.lower() == name.lower() and coin.NET.lower() == net.lower()): return coin raise CoinError('unknown coin {} and network {} combination' .format(name, net)) @classmethod def sanitize_url(cls, url): # Remove surrounding ws and trailing /s url = url.strip().rstrip('/') match = cls.RPC_URL_REGEX.match(url) if not match: raise CoinError('invalid daemon URL: "{}"'.format(url)) if match.groups()[0] is None: url += ':{:d}'.format(cls.RPC_PORT) if not url.startswith('http://'): url = 'http://' + url return url + '/' @classmethod def daemon_urls(cls, urls): return [cls.sanitize_url(url) for url in urls.split(',')] @cachedproperty def hash168_handlers(cls): return ScriptPubKey.PayToHandlers( address = cls.P2PKH_hash168_from_hash160, script_hash = cls.P2SH_hash168_from_hash160, pubkey = cls.P2PKH_hash168_from_pubkey, unspendable = cls.hash168_from_unspendable, strange = cls.hash168_from_strange, ) @classmethod def hash168_from_script(cls): '''Returns a function that is passed a script to return a hash168.''' return partial(ScriptPubKey.pay_to, cls.hash168_handlers) @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.coin_classes(): if verbytes == coin.XPUB_VERBYTES: return True, coin if verbytes == coin.XPRV_VERBYTES: return False, coin raise CoinError('version bytes unrecognised') @classmethod def address_to_hash168(cls, addr): '''Return a 21-byte hash given an address. This is the hash160 prefixed by the address version byte. ''' result = Base58.decode_check(addr) if len(result) != 21: raise CoinError('invalid address: {}'.format(addr)) return result @classmethod def hash168_to_address(cls, hash168): '''Return an address given a 21-byte hash.''' return Base58.encode_check(hash168) @classmethod def hash168_from_unspendable(cls): '''Return a hash168 for an unspendable script.''' return None @classmethod def hash168_from_strange(cls, script): '''Return a hash168 for a strange script.''' return bytes([cls.STRANGE_VERBYTE]) + hash160(script) @classmethod def P2PKH_hash168_from_hash160(cls, hash160): '''Return a hash168 if hash160 is 160 bits otherwise None.''' if len(hash160) == 20: return bytes([cls.P2PKH_VERBYTE]) + hash160 return None @classmethod def P2PKH_hash168_from_pubkey(cls, pubkey): return cls.P2PKH_hash168_from_hash160(hash160(pubkey)) @classmethod def P2PKH_address_from_hash160(cls, hash160): '''Return a P2PKH address given a public key.''' assert len(hash160) == 20 return Base58.encode_check(cls.P2PKH_hash168_from_hash160(hash160)) @classmethod def P2PKH_address_from_pubkey(cls, pubkey): '''Return a coin address given a public key.''' return cls.P2PKH_address_from_hash160(hash160(pubkey)) @classmethod def P2SH_hash168_from_hash160(cls, hash160): '''Return a hash168 if hash160 is 160 bits otherwise None.''' if len(hash160) == 20: return bytes([cls.P2SH_VERBYTE]) + hash160 return None @classmethod def P2SH_address_from_hash160(cls, hash160): '''Return a coin address given a hash160.''' assert len(hash160) == 20 return Base58.encode_check(cls.P2SH_hash168_from_hash160(hash160)) @classmethod def multisig_address(cls, m, pubkeys): '''Return the P2SH address for an M of N multisig transaction. Pass the N pubkeys of which M are needed to sign it. If generating an address for a wallet, it is the caller's responsibility to sort them to ensure order does not matter for, e.g., wallet recovery. ''' script = cls.pay_to_multisig_script(m, pubkeys) return cls.P2SH_address_from_hash160(hash160(script)) @classmethod def pay_to_multisig_script(cls, m, pubkeys): '''Return a P2SH script for an M of N multisig transaction.''' return ScriptPubKey.multisig_script(m, pubkeys) @classmethod def pay_to_pubkey_script(cls, pubkey): '''Return a pubkey script that pays to a pubkey. Pass the raw pubkey bytes (length 33 or 65). ''' return ScriptPubKey.P2PK_script(pubkey) @classmethod def pay_to_address_script(cls, address): '''Return a pubkey script that pays to a pubkey hash. Pass the address (either P2PKH or P2SH) in base58 form. ''' raw = Base58.decode_check(address) # Require version byte plus hash160. verbyte = -1 if len(raw) == 21: verbyte, hash_bytes = raw[0], raw[1:] if verbyte == cls.P2PKH_VERYBYTE: return ScriptPubKey.P2PKH_script(hash_bytes) if verbyte == cls.P2SH_VERBYTE: return ScriptPubKey.P2SH_script(hash_bytes) raise CoinError('invalid address: {}'.format(address)) @classmethod def prvkey_WIF(privkey_bytes, compressed): '''Return the private key encoded in Wallet Import Format.''' payload = bytearray([cls.WIF_BYTE]) + privkey_bytes if compressed: payload.append(0x01) return Base58.encode_check(payload) @classmethod def header_hash(cls, header): '''Given a header return hash''' return double_sha256(header) @classmethod def header_prevhash(cls, header): '''Given a header return previous hash''' return header[4:36] @classmethod def read_block(cls, block): '''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 @classmethod def electrum_header(cls, header, height): version, = struct.unpack('