Last active
August 17, 2022 14:17
-
-
Save parodyBit/3a19e357403c85d31cfe4543d808c891 to your computer and use it in GitHub Desktop.
bech32 encoder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a bech32 encoder | |
# run with command line arguments | |
# $ python3 bech32_encoder.py <prefix> <address> | |
# $ python3 bech32_encoder.py wit 0x000000000000000000000000000000000000dead | |
import sys | |
from typing import List, AnyStr | |
import unicodedata | |
from enum import Enum | |
def bytes_to_int(bts: bytes) -> int: | |
return int.from_bytes(bts, 'big') | |
def int_to_bytes(i: int) -> bytes: | |
length = max(1, (i.bit_length() + 7) // 8) | |
return i.to_bytes(length, 'big') | |
Bytes = List[int] | |
class BaseConversionError(Exception): | |
pass | |
def convert_bits(data: Bytes, from_bits: int, to_bits: int, pad=True) -> Bytes: | |
"""General power-of-2 base conversion.""" | |
acc = 0 | |
bits = 0 | |
ret = [] | |
max_v = (1 << to_bits) - 1 | |
max_acc = (1 << (from_bits + to_bits - 1)) - 1 | |
for value in data: | |
if value < 0 or (value >> from_bits): | |
raise BaseConversionError | |
acc = ((acc << from_bits) | value) & max_acc | |
bits += from_bits | |
while bits >= to_bits: | |
bits -= to_bits | |
ret.append((acc >> bits) & max_v) | |
if pad: | |
if bits: | |
ret.append((acc << (to_bits - bits)) & max_v) | |
elif bits >= from_bits or ((acc << (to_bits - bits)) & max_v): | |
raise BaseConversionError | |
return ret | |
def normalize_string(txt: AnyStr) -> str: | |
if isinstance(txt, bytes): | |
utxt = txt.decode('utf8') | |
elif isinstance(txt, str): | |
utxt = txt | |
else: | |
raise TypeError('String value expected') | |
return unicodedata.normalize('NFKD', utxt) | |
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' | |
CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' | |
BASE = len(ALPHABET) | |
class Bech32Encoding(Enum): | |
"""Enumeration type to list the various supported encodings.""" | |
BECH32 = 1 | |
BECH32M = 2 | |
class Bech32DecodeError(Exception): | |
pass | |
def bech32_poly_mod(values) -> int: | |
"""Internal function that computes the Bech32 checksum.""" | |
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] | |
chk = 1 | |
for value in values: | |
top = chk >> 25 | |
chk = (chk & 0x1ffffff) << 5 ^ value | |
for i in range(5): | |
chk ^= generator[i] if ((top >> i) & 1) else 0 | |
return chk | |
def bech32_hrp_expand(hrp: str) -> List[int]: | |
"""Expand the HRP into values for checksum computation.""" | |
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] | |
def bech32_verify_checksum(hrp: str, data) -> bool: | |
"""Verify a checksum given HRP and converted data characters.""" | |
return bech32_poly_mod(bech32_hrp_expand(hrp) + data) == 1 | |
def bech32_create_checksum(hrp: str, data): | |
"""Compute the checksum values given HRP and data.""" | |
values = bech32_hrp_expand(hrp) + data | |
polymod = bech32_poly_mod(values + [0, 0, 0, 0, 0, 0]) ^ 1 | |
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] | |
def bech32_decode_address(bech: str): | |
bech, separator = bech.lower(), bech.find('1') | |
hrp_ = bech[:separator] | |
data = [CHARSET.find(x) for x in bech[separator + 1:]] | |
decoded = data[:-6] | |
try: | |
if any(ord(x) < 33 or ord(x) > 126 for x in bech): | |
raise Bech32DecodeError('Character outside US-ASCII [33-126] range') | |
if (bech.lower() != bech) and (bech.upper() != bech): | |
raise Bech32DecodeError('Mixed upper and lower case') | |
if separator == 0: | |
raise Bech32DecodeError('Empty human readable part') | |
elif separator == -1: | |
raise Bech32DecodeError('No separator character') | |
elif separator + 7 > len(bech): | |
raise Bech32DecodeError('Checksum too short') | |
if not all(x in CHARSET for x in bech[separator + 1:]): | |
raise Bech32DecodeError('Character not in charset') | |
if not bech32_verify_checksum(hrp_, data): | |
raise Bech32DecodeError('Invalid checksum') | |
if decoded is None or len(decoded) < 2: | |
raise Bech32DecodeError('Witness program too short') | |
except Bech32DecodeError as error: | |
print(error) | |
b256 = convert_bits(decoded, from_bits=5, to_bits=8, pad=True) | |
return b256 | |
def bech32_encode_address(hrp: str, data: str) -> str: | |
hex_bytes = [b for b in bytes.fromhex(data)] | |
data = convert_bits(data=hex_bytes, from_bits=8, to_bits=5, pad=False) | |
checksum = bech32_create_checksum(hrp=hrp, data=data) | |
combined = data + checksum | |
return normalize_string(hrp + '1' + ''.join([CHARSET[i] for i in combined])) | |
if __name__ == '__main__': | |
public_key = sys.argv[2] | |
prefix = sys.argv[1] | |
if public_key.startswith('0x'): | |
public_key = public_key[2:] | |
address = bech32_encode_address(prefix, public_key) | |
print(address) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment