Pure functions docs

This commit is contained in:
4tochka 2018-06-19 01:22:26 +04:00
parent bf906e24e2
commit 4ecdadc0f4
5 changed files with 301 additions and 142 deletions

View File

@ -19,6 +19,7 @@ sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath('../_static/'))
sys.path.insert(0, os.path.abspath('../../_static/'))
sys.path.insert(0, os.path.abspath('../../pybtc/'))
sys.path.insert(0, os.path.abspath('./_static/'))

View File

@ -0,0 +1,49 @@
==============
Pure functions
==============
Base function primitives implemeted in functional programming paradigm.
Key management
==============
Tools for private and public key managment
Private key
-----------
.. autofunction:: pybtc.create_private_key
.. autofunction:: pybtc.private_key_to_wif
.. autofunction:: pybtc.wif_to_private_key
.. autofunction:: pybtc.is_wif_valid
Public key
----------
.. WARNING::
Using uncompressed public keys is
`deprecated <https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#restrictions-on-public-key-type>`_
in a new SEGWIT address format.
To avoid potential future funds loss, users MUST NOT use uncompressed keys
in version 0 witness programs. Use uncompressed keys only for backward
compatibilitylegacy in legacy address format (PUBKEY, P2PKH).
.. autofunction:: pybtc.private_to_public_key
.. autofunction:: pybtc.is_public_key_valid
Addresses
=========
.. autofunction:: pybtc.hash_to_address
.. autofunction:: pybtc.address_to_hash
.. autofunction:: pybtc.public_key_to_address
.. autofunction:: pybtc.address_type
.. autofunction:: pybtc.address_to_script
.. autofunction:: pybtc.is_address_valid

View File

@ -111,4 +111,4 @@ Table Of Contents
:name: mastertoc
:maxdepth: 2
tools.rst
functional.rst

View File

@ -1,5 +0,0 @@
Tools
======
.. autofunction:: pybtc.tools.sign_message

View File

@ -9,33 +9,53 @@ import math
import io
# Bitcoin keys
#
def create_private_key(compressed=True, testnet=False, wif=True):
# Key management
def create_private_key(compressed=True, testnet=False, wif=True, hex=False):
"""
:return: 32 bytes private key
Create private key
:param compressed: (optional) Type of public key, by default set to compressed.
Using uncompressed public keys is deprecated in new SEGWIT addresses,
use this option only for backward compatibility.
:param testnet: (optional) flag for testnet network, by default is False.
:param wif: (optional) If set to True return key in WIF format, by default is True.
:param hex: (optional) If set to True return key in HEX format, by default is False.
:return: Private key in wif format (default), hex encoded byte string in case of hex flag or
raw bytes string in case wif and hex flags set to False.
"""
a = random.SystemRandom().randint(0, MAX_INT_PRIVATE_KEY)
i = int((time.time() % 0.01)*100000)
h = a.to_bytes(32, byteorder="big")
while True:
# more entropy from system timer and sha256 derivation
while i:
h = hashlib.sha256(h).digest()
if i > 1:
i -= 1
else:
if int.from_bytes(h, byteorder="big") < MAX_INT_PRIVATE_KEY:
break
if hex:
i -= 1
if not i and int.from_bytes(h, byteorder="big") > MAX_INT_PRIVATE_KEY:
i += 1
if wif:
return private_key_to_wif(h)
elif hex:
return hexlify(h).decode()
return h
def private_key_to_wif(h, compressed=True, testnet=False):
# uncompressed: 0x80 + [32-byte secret] + [4 bytes of Hash() of previous 33 bytes], base58 encoded
# compressed: 0x80 + [32-byte secret] + 0x01 + [4 bytes of Hash() previous 34 bytes], base58 encoded
if type(h) == str:
"""
Encode private key in HEX or RAW bytes format to WIF format.
:param h: private key 32 byte string or HEX encoded string.
:param compressed: (optional) flag of public key compressed format, by default set to True.
:param testnet: (optional) flag for testnet network, by default is False.
:return: Private key in WIF format
"""
# uncompressed: 0x80 + [32-byte secret] + [4 bytes of Hash() of previous 33 bytes], base58 encoded.
# compressed: 0x80 + [32-byte secret] + 0x01 + [4 bytes of Hash() previous 34 bytes], base58 encoded.
if isinstance(h, str):
h = unhexlify(h)
assert len(h) == 32
if len(h) != 32 and isinstance(h, bytes):
raise TypeError("private key must be a 32 bytes or hex encoded string")
if testnet:
h = TESTNET_PRIVATE_KEY_BYTE_PREFIX + h
else:
@ -46,8 +66,15 @@ def private_key_to_wif(h, compressed=True, testnet=False):
return encode_base58(h)
def wif_to_private_key(h, hex=False):
assert is_wif_valid(h)
def wif_to_private_key(h, hex=True):
"""
Decode WIF private key to bytes string or HEX encoded string
:param hex: (optional) if set to True return key in HEX format, by default is True.
:return: Private key HEX encoded string or raw bytes string.
"""
if not is_wif_valid(h):
raise TypeError("invalid wif key")
h = decode_base58(h)
if hex:
return hexlify(h[1:33]).decode()
@ -55,7 +82,14 @@ def wif_to_private_key(h, hex=False):
def is_wif_valid(wif):
assert type(wif) == str
"""
Check is private key in WIF format string is valid.
:param wif: private key in WIF format string.
:return: boolean
"""
if not isinstance(wif, str):
raise TypeError("invalid wif key")
if wif[0] not in PRIVATE_KEY_PREFIX_LIST:
return False
try:
@ -75,10 +109,20 @@ def is_wif_valid(wif):
def private_to_public_key(private_key, compressed=True, hex=True):
if type(private_key)!= bytes:
if type(private_key) == bytearray:
"""
Get public key from private key using ECDSA secp256k1
:param private_key: private key in WIF, HEX or bytes.
:param compressed: (optional) flag of public key compressed format, by default set to True.
In case private_key in WIF format, this flag is set in accordance with
the key format specified in WIF string.
:param hex: (optional) if set to True return key in HEX format, by default is True.
:return: 33/65 bytes public key in HEX or bytes string
"""
if not isinstance(private_key, bytes):
if isinstance(private_key, bytearray):
private_key = bytes(private_key)
elif type(private_key) == str:
elif isinstance(private_key, str):
if not is_wif_valid(private_key):
private_key = unhexlify(private_key)
else:
@ -90,18 +134,26 @@ def private_to_public_key(private_key, compressed=True, hex=True):
raise TypeError("private key must be a bytes or WIF or hex encoded string")
pubkey_ptr = ffi.new('secp256k1_pubkey *')
r = secp256k1.secp256k1_ec_pubkey_create(ECDSA_CONTEXT_ALL, pubkey_ptr, private_key)
assert r == 1
if not r:
raise RuntimeError("secp256k1 error")
len_key = 33 if compressed else 65
pubkey = ffi.new('char [%d]' % len_key)
outlen = ffi.new('size_t *', len_key)
compflag = EC_COMPRESSED if compressed else EC_UNCOMPRESSED
r = secp256k1.secp256k1_ec_pubkey_serialize(ECDSA_CONTEXT_VERIFY, pubkey, outlen, pubkey_ptr, compflag)
assert r == 1
pub = bytes(ffi.buffer(pubkey, len_key))
if not r:
raise RuntimeError("secp256k1 error")
return hexlify(pub).decode() if hex else pub
def is_valid_public_key(key):
def is_public_key_valid(key):
"""
Check public key is valid.
:param key: public key in HEX or bytes string format.
:return: boolean
"""
if isinstance(key, str):
key = unhexlify(key)
if len(key) < 33:
@ -114,18 +166,30 @@ def is_valid_public_key(key):
return True
#
# Bitcoin addresses
#
# Addresses
def hash_to_address(address_hash, testnet=False, script_hash=False, witness_version=0):
"""
Get address from public key/script hash. In case PUBKEY, P2PKH, P2PKH public key/script hash is SHA256+RIPEMD160,
P2WSH script hash is SHA256.
def hash_to_address(address_hash, testnet=False,
script_hash=False, witness_version=0):
if type(address_hash) == str:
:param address_hash: public key hash or script hash in HEX or bytes string format.
:param testnet: (optional) flag for testnet network, by default is False.
:param script_hash: (optional) flag for script hash (P2SH address), by default is False.
:param witness_version: (optional) witness program version, by default is 0, for legacy
address format use None.
:return: address in base58 or bech32 format.
"""
if isinstance(address_hash, str):
address_hash = unhexlify(address_hash)
if not isinstance(address_hash, bytes):
raise TypeError("address hash must be HEX encoded string or bytes")
if not script_hash:
if witness_version is None:
assert len(address_hash) == 20
if len(address_hash) != 20:
raise TypeError("address hash length incorrect")
if testnet:
prefix = TESTNET_ADDRESS_BYTE_PREFIX
else:
@ -134,7 +198,9 @@ def hash_to_address(address_hash, testnet=False,
address_hash += double_sha256(address_hash)[:4]
return encode_base58(address_hash)
else:
assert len(address_hash) in (20,32)
if len(address_hash) not in (20, 32):
raise TypeError("address hash length incorrect")
if witness_version is None:
if testnet:
prefix = TESTNET_SCRIPT_ADDRESS_BYTE_PREFIX
@ -143,19 +209,59 @@ def hash_to_address(address_hash, testnet=False,
address_hash = prefix + address_hash
address_hash += double_sha256(address_hash)[:4]
return encode_base58(address_hash)
if testnet:
prefix = TESTNET_SEGWIT_ADDRESS_BYTE_PREFIX
hrp = TESTNET_SEGWIT_ADDRESS_PREFIX
else:
prefix = MAINNET_SEGWIT_ADDRESS_BYTE_PREFIX
hrp = MAINNET_SEGWIT_ADDRESS_PREFIX
address_hash = witness_version.to_bytes(1, "big") + rebase_8_to_5(address_hash)
checksum = bech32_polymod(prefix + address_hash + b"\x00" * 6)
checksum = rebase_8_to_5(checksum.to_bytes(5, "big"))[2:]
return "%s1%s" % (hrp, rebase_5_to_32(address_hash + checksum).decode())
def address_to_hash(address, hex=False):
def public_key_to_address(pubkey, testnet=False, p2sh_p2wpkh=False, witness_version=0):
"""
Get address from public key/script hash. In case PUBKEY, P2PKH, P2PKH public key/script hash is SHA256+RIPEMD160,
P2WSH script hash is SHA256.
:param pubkey: public key HEX or bytes string format.
:param testnet: (optional) flag for testnet network, by default is False.
:param p2sh_p2wpkh: (optional) flag for P2WPKH inside P2SH address, by default is False.
:param witness_version: (optional) witness program version, by default is 0, for legacy
address format use None.
:return: address in base58 or bech32 format.
"""
if isinstance(pubkey, str):
pubkey = unhexlify(pubkey)
if not isinstance(pubkey, bytes):
raise TypeError("public key invalid")
if p2sh_p2wpkh:
if len(pubkey) != 33:
raise TypeError("public key invalid")
h = hash160(b'\x00\x14' + hash160(pubkey))
witness_version = None
else:
if witness_version is not None:
if len(pubkey) != 33:
raise TypeError("public key invalid")
h = hash160(pubkey)
return hash_to_address(h, testnet=testnet,
script_hash=p2sh_p2wpkh,
witness_version=witness_version)
def address_to_hash(address, hex=True):
"""
Get address hash from base58 or bech32 address format.
:param address: address in base58 or bech32 format.
:param hex: (optional) If set to True return key in HEX format, by default is True.
:return: script in HEX or bytes string
"""
if address[0] in ADDRESS_PREFIX_LIST:
h = decode_base58(address)[1:-4]
elif address[:2] in (MAINNET_SEGWIT_ADDRESS_PREFIX,
@ -164,19 +270,17 @@ def address_to_hash(address, hex=False):
h = rebase_5_to_8(rebase_32_to_5(address)[1:-6], False)
else:
return None
if hex:
return h.hex()
else:
return h
def get_witness_version(address):
address = address.split("1")[1]
h = rebase_32_to_5(address)
return h[0]
return h.hex() if hex else h
def address_type(address, num=False):
"""
Get address type.
:param address: address in base58 or bech32 format.
:param num: (optional) If set to True return type in numeric format, by default is False.
:return: address type in string or numeric format.
"""
if address[0] in (TESTNET_SCRIPT_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX):
t = 'P2SH'
@ -198,6 +302,12 @@ def address_type(address, num=False):
def address_net_type(address):
"""
Get address network type.
:param address: address in base58 or bech32 format.
:return: address network type in string format or None.
"""
if address[0] in (MAINNET_SCRIPT_ADDRESS_PREFIX,
MAINNET_ADDRESS_PREFIX):
return "mainnet"
@ -212,16 +322,14 @@ def address_net_type(address):
return None
def script_to_hash(s, witness=False, hex=False):
if type(s) == str:
s = unhexlify(s)
if witness:
return sha256(s, hex)
else:
return hash160(s, hex)
def address_to_script(address, hex=False):
"""
Get public key script from address.
:param address: address in base58 or bech32 format.
:param hex: (optional) If set to True return key in HEX format, by default is True.
:return: public key script in HEX or bytes string
"""
if address[0] in (TESTNET_SCRIPT_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX):
s = [BYTE_OPCODE["OP_HASH160"],
@ -244,7 +352,7 @@ def address_to_script(address, hex=False):
bytes([len(h)]),
h]
else:
assert False
raise TypeError("address invalid")
s = b''.join(s)
return hexlify(s).decode() if hex else s
@ -254,27 +362,84 @@ def public_key_to_p2sh_p2wpkh_script(pubkey):
return b'\x00\x14' + hash160(pubkey)
def public_key_to_address(pubkey, testnet=False,
p2sh_p2wpkh=False,
witness_version=0):
if type(pubkey) == str:
pubkey = unhexlify(pubkey)
if p2sh_p2wpkh:
assert len(pubkey) == 33
h = hash160(b'\x00\x14' + hash160(pubkey))
witness_version = None
else:
if witness_version is not None:
assert len(pubkey) == 33
h = hash160(pubkey)
return hash_to_address(h, testnet=testnet,
script_hash=p2sh_p2wpkh,
witness_version=witness_version)
def is_address_valid(address, testnet=False):
"""
Check is address valid.
:param address: address in base58 or bech32 format.
:param testnet: (optional) flag for testnet network, by default is False.
:return: boolean
"""
if not address or type(address) != str:
return False
if address[0] in (MAINNET_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX_2,
TESTNET_SCRIPT_ADDRESS_PREFIX):
if testnet:
if address[0] not in (TESTNET_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX_2,
TESTNET_SCRIPT_ADDRESS_PREFIX):
return False
else:
if address[0] not in (MAINNET_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX):
return False
h = decode_base58(address)
if len(h) != 25:
return False
checksum = h[-4:]
if double_sha256(h[:-4])[:4] != checksum:
return False
return True
elif address[:2].lower() in (TESTNET_SEGWIT_ADDRESS_PREFIX,
MAINNET_SEGWIT_ADDRESS_PREFIX):
if len(address) not in (42, 62):
return False
try:
prefix, payload = address.split('1')
except:
return False
upp = True if prefix[0].isupper() else False
for i in payload[1:]:
if upp:
if not i.isupper() or i not in base32charset_upcase:
return False
else:
if i.isupper() or i not in base32charset:
return False
payload = payload.lower()
prefix = prefix.lower()
if testnet:
if prefix != TESTNET_SEGWIT_ADDRESS_PREFIX:
return False
stripped_prefix = TESTNET_SEGWIT_ADDRESS_BYTE_PREFIX
else:
if prefix != MAINNET_SEGWIT_ADDRESS_PREFIX:
return False
stripped_prefix = MAINNET_SEGWIT_ADDRESS_BYTE_PREFIX
d = rebase_32_to_5(payload)
address_hash = d[:-6]
checksum = d[-6:]
checksum2 = bech32_polymod(stripped_prefix + address_hash + b"\x00" * 6)
checksum2 = rebase_8_to_5(checksum2.to_bytes(5, "big"))[2:]
if checksum != checksum2:
return False
return True
def get_witness_version(address):
address = address.split("1")[1]
h = rebase_32_to_5(address)
return h[0]
# Script
def parse_script(script, segwit=True):
if not script:
return {"nType": 7, "type": "NON_STANDARD", "reqSigs": 0, "script": b""}
return {"nType": 7, "type": "NON_STANDARD", "reqSigs": 0, "script": b""}
if type(script) == str:
try:
script = unhexlify(script)
@ -288,12 +453,12 @@ def parse_script(script, segwit=True):
if l == 34 and script[0] == 0:
return {"nType": 6, "type": "P2WSH", "reqSigs": None, "addressHash": script[2:]}
if l == 25 and \
script[:2] == b"\x76\xa9" and \
script[-2:] == b"\x88\xac":
script[:2] == b"\x76\xa9" and \
script[-2:] == b"\x88\xac":
return {"nType": 0, "type": "P2PKH", "reqSigs": 1, "addressHash": script[3:-2]}
if l == 23 and \
script[0] == 169 and \
script[-1] == 135:
script[0] == 169 and \
script[-1] == 135:
return {"nType": 1, "type": "P2SH", "reqSigs": None, "addressHash": script[2:-1]}
if l == 67 and script[-1] == 172:
return {"nType": 2, "type": "PUBKEY", "reqSigs": 1, "addressHash": hash160(script[1:-1])}
@ -316,7 +481,7 @@ def parse_script(script, segwit=True):
break
s += 1
if c == script[-2] - 80:
return {"nType": 4, "type": "MULTISIG", "reqSigs": script[0] - 80, "script": script}
return {"nType": 4, "type": "MULTISIG", "reqSigs": script[0] - 80, "script": script}
s, m, n, last, req_sigs = 0, 0, 0, 0, 0
while l - s > 0:
@ -364,7 +529,7 @@ def parse_script(script, segwit=True):
if last:
last -= 1
s += 1
return {"nType": 7, "type": "NON_STANDARD", "reqSigs": req_sigs, "script": script}
return {"nType": 7, "type": "NON_STANDARD", "reqSigs": req_sigs, "script": script}
def decode_script(script, asm=False):
@ -380,7 +545,7 @@ def decode_script(script, asm=False):
while l - s > 0:
if script[s] < 0x4c and script[s]:
if asm:
result.append(hexlify(script[s+1:s+1 +script[s]]).decode())
result.append(hexlify(script[s + 1:s + 1 + script[s]]).decode())
else:
result.append('[%s]' % script[s])
s += script[s] + 1
@ -445,7 +610,7 @@ def delete_from_script(script, sub_script):
k = s
else:
t = stack.pop(0)
result.append(script[k:k+t])
result.append(script[k:k + t])
k += t
if script[k:s][:ls] == sub_script:
if s - k > ls:
@ -456,64 +621,13 @@ def delete_from_script(script, sub_script):
return b''.join(result) if not s_hex else hexlify(b''.join(result)).decode()
def is_address_valid(address, testnet=False):
if not address or type(address) != str:
return False
if address[0] in (MAINNET_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX_2,
TESTNET_SCRIPT_ADDRESS_PREFIX):
if testnet:
if address[0] not in (TESTNET_ADDRESS_PREFIX,
TESTNET_ADDRESS_PREFIX_2,
TESTNET_SCRIPT_ADDRESS_PREFIX):
return False
else:
if address[0] not in (MAINNET_ADDRESS_PREFIX,
MAINNET_SCRIPT_ADDRESS_PREFIX):
return False
h = decode_base58(address)
if len(h) != 25:
return False
checksum = h[-4:]
if double_sha256(h[:-4])[:4] != checksum:
return False
return True
elif address[:2].lower() in (TESTNET_SEGWIT_ADDRESS_PREFIX,
MAINNET_SEGWIT_ADDRESS_PREFIX):
if len(address) not in (42, 62):
return False
try:
prefix, payload = address.split('1')
except:
return False
upp = True if prefix[0].isupper() else False
for i in payload[1:]:
if upp:
if not i.isupper() or i not in base32charset_upcase:
return False
else:
if i.isupper() or i not in base32charset:
return False
payload = payload.lower()
prefix = prefix.lower()
if testnet:
if prefix != TESTNET_SEGWIT_ADDRESS_PREFIX:
return False
stripped_prefix = TESTNET_SEGWIT_ADDRESS_BYTE_PREFIX
else:
if prefix != MAINNET_SEGWIT_ADDRESS_PREFIX:
return False
stripped_prefix = MAINNET_SEGWIT_ADDRESS_BYTE_PREFIX
d = rebase_32_to_5(payload)
address_hash = d[:-6]
checksum = d[-6:]
checksum2 = bech32_polymod(stripped_prefix + address_hash + b"\x00" * 6)
checksum2 = rebase_8_to_5(checksum2.to_bytes(5, "big"))[2:]
if checksum != checksum2:
return False
return True
def script_to_hash(s, witness=False, hex=False):
if type(s) == str:
s = unhexlify(s)
if witness:
return sha256(s, hex)
else:
return hash160(s, hex)
#