hub/scribe/base58.py

112 lines
3 KiB
Python
Raw Normal View History

2022-03-08 17:01:19 +01:00
import hashlib
def sha256(x):
""" Simple wrapper of hashlib sha256. """
return hashlib.sha256(x).digest()
def double_sha256(x):
""" SHA-256 of SHA-256, as used extensively in bitcoin. """
return sha256(sha256(x))
class Base58Error(Exception):
""" Exception used for Base58 errors. """
_CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def _iter_encode(be_bytes: bytes):
value = int.from_bytes(be_bytes, 'big')
while value:
value, mod = divmod(value, 58)
yield _CHARS[mod]
for byte in be_bytes:
if byte != 0:
break
yield '1'
def b58_encode(be_bytes: bytes):
return ''.join(_iter_encode(be_bytes))[::-1]
class Base58:
""" Class providing base 58 functionality. """
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
assert len(chars) == 58
char_map = {c: n for n, c in enumerate(chars)}
@classmethod
def char_value(cls, c):
val = cls.char_map.get(c)
if val is None:
raise Base58Error(f'invalid base 58 character "{c}"')
return val
@classmethod
def decode(cls, txt):
""" Decodes txt into a big-endian bytearray. """
if isinstance(txt, memoryview):
txt = str(txt)
if isinstance(txt, bytes):
txt = txt.decode()
if not isinstance(txt, str):
raise TypeError(f'a string is required, got {type(txt).__name__}')
if not txt:
raise Base58Error('string cannot be empty')
value = 0
for c in txt:
value = value * 58 + cls.char_value(c)
result = value.to_bytes((value.bit_length() + 7) // 8, 'big')
# Prepend leading zero bytes if necessary
count = 0
for c in txt:
if c != '1':
break
count += 1
if count:
result = bytes((0,)) * count + result
return result
@classmethod
def _iter_encode(cls, be_bytes: bytes):
value = int.from_bytes(be_bytes, 'big')
while value:
value, mod = divmod(value, 58)
yield cls.chars[mod]
for byte in be_bytes:
if byte != 0:
break
yield '1'
@classmethod
def encode(cls, be_bytes):
return ''.join(cls._iter_encode(be_bytes))[::-1]
@classmethod
def decode_check(cls, txt, hash_fn=double_sha256):
""" Decodes a Base58Check-encoded string to a payload. The version prefixes it. """
be_bytes = cls.decode(txt)
result, check = be_bytes[:-4], be_bytes[-4:]
if check != hash_fn(result)[:4]:
raise Base58Error(f'invalid base 58 checksum for {txt}')
return result
@classmethod
def encode_check(cls, payload, hash_fn=double_sha256):
""" Encodes a payload bytearray (which includes the version byte(s))
into a Base58Check string."""
be_bytes = payload + hash_fn(payload)[:4]
return b58_encode(be_bytes)