r"""Bech32 addresses (Wuille and Maxwell, BIP173, 2017) and Bech32m (Wuille, BIP350, 2020).
A Bech32 string is a human-readable part such as ``bc``, the separator
``1``, and data in 32 lower-case characters chosen to avoid look-alikes,
ending in a 6-character checksum. The checksum is a BCH code over GF(32):
the data and checksum, read as a polynomial with 5-bit coefficients, must
leave a fixed remainder modulo a generator g(x) of degree 6,
.. math::
\mathrm{polymod}(\mathrm{expand}(hrp) \,\|\, data \,\|\, checksum) = 1 .
g(x) was chosen by a search of 159,605 BCH codes for the best detection of
the errors people make: every error affecting at most 4 characters is
detected in strings of up to 89 characters, which Base58Check's 32-bit hash
cannot promise.
In 2019 a weakness was found: when a string ends in ``p``, inserting or
deleting ``q`` characters just before it leaves the checksum valid, because
``q`` is the value 0 and the constant 1 lets the polynomial absorb factors
of x. Segregated-witness version 0 survives, since its programs must be 20
or 32 bytes, but later versions allow other lengths. Bech32m changes only
the constant, to ``0x2bc830a3``, and is required from witness version 1
(Taproot) on.
"""
from collections.abc import Sequence
from blockchainkit._validation import integer
from blockchainkit.wallets.core.base import Bech32Decoding, WitnessProgram
from blockchainkit.wallets.utils.keys import PublicKey, compress
from blockchainkit.wallets.utils.ripemd160 import hash160
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
"""str: The 32 data characters; character i has the 5-bit value i."""
BECH32_CONSTANTS = {"bech32": 1, "bech32m": 0x2BC830A3}
"""dict: The value a valid checksum leaves, for each encoding."""
_GENERATOR = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
[docs]
def polymod(values: Sequence[int]) -> int:
"""The remainder of a sequence of 5-bit values modulo Bech32's generator polynomial.
Each step shifts in one value; the five constants are g(x) times 1, 2,
4, 8 and 16 in GF(32), added for each bit that overflows.
"""
checksum = 1
for value in values:
top = checksum >> 25
checksum = (checksum & 0x1FFFFFF) << 5 ^ value
for i in range(5):
if top >> i & 1:
checksum ^= _GENERATOR[i]
return checksum
def _expand(hrp: str) -> list[int]:
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
def _encoding(encoding: str) -> int:
if encoding not in BECH32_CONSTANTS:
raise ValueError(f"encoding must be one of {sorted(BECH32_CONSTANTS)}")
return BECH32_CONSTANTS[encoding]
[docs]
def bech32_encode(hrp: str, data: Sequence[int], encoding: str = "bech32") -> str:
"""Write ``hrp``, ``1``, the 5-bit ``data`` and a 6-character checksum.
Examples
--------
>>> from blockchainkit.wallets import bech32_encode
>>> bech32_encode("a", [])
'a12uel5l'
>>> bech32_encode("a", [], "bech32m")
'a1lqfn3a'
"""
constant = _encoding(encoding)
if not isinstance(hrp, str) or not 1 <= len(hrp) <= 83:
raise ValueError("the human-readable part has 1 to 83 characters")
if any(not 33 <= ord(c) <= 126 for c in hrp) or hrp.lower() != hrp:
raise ValueError("the human-readable part is lower-case printable ASCII")
values = list(data)
for value in values:
integer(value, "data value")
if value > 31:
raise ValueError("data values are 5 bits")
if len(hrp) + 7 + len(values) > 90:
raise ValueError("a Bech32 string has at most 90 characters")
mod = polymod([*_expand(hrp), *values, 0, 0, 0, 0, 0, 0]) ^ constant
checksum = [mod >> 5 * (5 - i) & 31 for i in range(6)]
return hrp + "1" + "".join(CHARSET[v] for v in values + checksum)
[docs]
def bech32_decode(text: str) -> Bech32Decoding:
"""Check a Bech32 or Bech32m string and split it into its parts.
Raises
------
ValueError
Mixed case, a bad character or length, no separator, or a checksum
that matches neither encoding.
"""
if not isinstance(text, str):
raise TypeError("text must be a string")
if text.lower() != text and text.upper() != text:
raise ValueError("mixed case")
if len(text) > 90 or any(not 33 <= ord(c) <= 126 for c in text):
raise ValueError("at most 90 printable ASCII characters")
text = text.lower()
split = text.rfind("1")
if split < 1 or split + 7 > len(text):
raise ValueError("needs a human-readable part, 1, and at least 6 data characters")
hrp, part = text[:split], text[split + 1 :]
if any(c not in CHARSET for c in part):
raise ValueError("a data character is not in the Bech32 alphabet")
values = [CHARSET.index(c) for c in part]
remainder = polymod(_expand(hrp) + values)
for encoding, constant in BECH32_CONSTANTS.items():
if remainder == constant:
return Bech32Decoding(hrp, tuple(values[:-6]), encoding)
raise ValueError("checksum mismatch: the string was mistyped")
[docs]
def convert_bits(
data: Sequence[int], source: int, target: int, pad: bool = True
) -> tuple[int, ...]:
"""Regroup a sequence of ``source``-bit values into ``target``-bit values.
With ``pad``, the last group is filled with zero bits; without, leftover
bits must be fewer than ``target`` and zero.
"""
accumulator, bits, out = 0, 0, []
for value in data:
if not 0 <= value < 1 << source:
raise ValueError(f"values must be {source}-bit")
accumulator = accumulator << source | value
bits += source
while bits >= target:
bits -= target
out.append(accumulator >> bits & (1 << target) - 1)
if pad and bits:
out.append(accumulator << (target - bits) & (1 << target) - 1)
elif not pad and (bits >= source or accumulator << (target - bits) & (1 << target) - 1):
raise ValueError("leftover padding bits")
return tuple(out)
[docs]
def segwit_encode(hrp: str, version: int, program: bytes) -> str:
"""A segregated-witness address: Bech32 for version 0, Bech32m from version 1.
Examples
--------
>>> from blockchainkit.wallets import segwit_encode
>>> segwit_encode("bc", 0, bytes.fromhex("751e76e8199196d454941c45d1b3a323f1433bd6"))
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
"""
_check_program(version, program)
encoding = "bech32" if version == 0 else "bech32m"
return bech32_encode(hrp, [version, *convert_bits(program, 8, 5)], encoding)
[docs]
def segwit_decode(hrp: str, address: str) -> WitnessProgram:
"""Decode and check a segregated-witness address for network ``hrp``.
Raises
------
ValueError
The checksum fails, the network differs, the program has a bad
length, or the encoding does not match the version.
"""
decoded = bech32_decode(address)
if decoded.hrp != hrp:
raise ValueError(f"address is for {decoded.hrp!r}, not {hrp!r}")
if not decoded.data:
raise ValueError("no witness version")
version = decoded.data[0]
program = bytes(convert_bits(decoded.data[1:], 5, 8, pad=False))
_check_program(version, program)
expected = "bech32" if version == 0 else "bech32m"
if decoded.encoding != expected:
raise ValueError(f"witness version {version} must use {expected}")
return WitnessProgram(version, program)
def _check_program(version: int, program: bytes) -> None:
integer(version, "version")
if version > 16:
raise ValueError("witness versions run from 0 to 16")
if not isinstance(program, bytes) or not 2 <= len(program) <= 40:
raise ValueError("a witness program has 2 to 40 bytes")
if version == 0 and len(program) not in (20, 32):
raise ValueError("a version-0 program is 20 or 32 bytes")
[docs]
def p2wpkh_address(public: PublicKey, hrp: str = "bc") -> str:
"""The native segregated-witness address of a key: version 0 and its ``HASH160``.
Examples
--------
>>> from blockchainkit.crypto import SECP256K1
>>> from blockchainkit.wallets import p2wpkh_address
>>> p2wpkh_address(SECP256K1.generator)
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
"""
return segwit_encode(hrp, 0, hash160(compress(public)))